from cStringIO import StringIO import contextlib import copy import logging import time import os from teuthology import misc as teuthology from teuthology import contextutil from teuthology.parallel import parallel from ..orchestra import run from ..orchestra.run import CommandFailedError log = logging.getLogger(__name__) # Should the RELEASE value get extracted from somewhere? RELEASE = "1-0" # This is intended to be a complete listing of ceph packages. If we're going # to hardcode this stuff, I don't want to do it in more than once place. PACKAGES = {} PACKAGES['ceph'] = {} PACKAGES['ceph']['deb'] = [ 'ceph', 'ceph-dbg', 'ceph-mds', 'ceph-mds-dbg', 'ceph-common', 'ceph-common-dbg', 'ceph-fuse', 'ceph-fuse-dbg', 'ceph-test', 'ceph-test-dbg', 'radosgw', 'radosgw-dbg', 'python-ceph', 'libcephfs1', 'libcephfs1-dbg', 'libcephfs-java', 'librados2', 'librados2-dbg', 'librbd1', 'librbd1-dbg', ] PACKAGES['ceph']['rpm'] = [ 'ceph-debuginfo', 'ceph-radosgw', 'ceph-test', 'ceph-devel', 'ceph', 'ceph-fuse', 'rest-bench', 'libcephfs_jni1', 'libcephfs1', 'python-ceph', ] def _run_and_log_error_if_fails(remote, args): """ Yet another wrapper around command execution. This one runs a command on the given remote, then, if execution fails, logs the error and re-raises. :param remote: the teuthology.orchestra.remote.Remote object :param args: list of arguments comprising the command the be executed :returns: None :raises: CommandFailedError """ response = StringIO() try: remote.run( args=args, stdout=response, stderr=response, ) except CommandFailedError: log.error(response.getvalue().strip()) raise def _get_config_value_for_remote(ctx, remote, config, key): """ Look through config, and attempt to determine the "best" value to use for a given key. For example, given: config = { 'all': {'branch': 'master'}, 'branch': 'next' } _get_config_value_for_remote(ctx, remote, config, 'branch') would return 'master'. :param ctx: the argparse.Namespace object :param remote: the teuthology.orchestra.remote.Remote object :param config: the config dict :param key: the name of the value to retrieve """ roles = ctx.cluster.remotes[remote] if 'all' in config: return config['all'].get(key) elif roles: for role in roles: if role in config and key in config[role]: return config[role].get(key) return config.get(key) def _get_uri(tag, branch, sha1): """ Set the uri -- common code used by both install and debian upgrade """ uri = None if tag: uri = 'ref/' + tag elif branch: uri = 'ref/' + branch elif sha1: uri = 'sha1/' + sha1 else: # FIXME: Should master be the default? log.debug("defaulting to master branch") uri = 'ref/master' return uri def _get_baseurlinfo_and_dist(ctx, remote, config): """ Through various commands executed on the remote, determines the distribution name and version in use, as well as the portion of the repo URI to use to specify which version of the project (normally ceph) to install.Example: {'arch': 'x86_64', 'dist': 'raring', 'dist_release': None, 'distro': 'Ubuntu', 'distro_release': None, 'flavor': 'basic', 'relval': '13.04', 'uri': 'ref/master'} :param ctx: the argparse.Namespace object :param remote: the teuthology.orchestra.remote.Remote object :param config: the config dict :returns: dict -- the information you want. """ retval = {} relval = None r = remote.run( args=['arch'], stdout=StringIO(), ) retval['arch'] = r.stdout.getvalue().strip() r = remote.run( args=['lsb_release', '-is'], stdout=StringIO(), ) retval['distro'] = r.stdout.getvalue().strip() r = remote.run( args=[ 'lsb_release', '-rs'], stdout=StringIO()) retval['relval'] = r.stdout.getvalue().strip() dist_name = None if ((retval['distro'] == 'CentOS') | (retval['distro'] == 'RedHatEnterpriseServer')): relval = retval['relval'] relval = relval[0:relval.find('.')] distri = 'centos' retval['distro_release'] = '%s%s' % (distri, relval) retval['dist'] = retval['distro_release'] dist_name = 'el' retval['dist_release'] = '%s%s' % (dist_name, relval) elif retval['distro'] == 'Fedora': distri = retval['distro'] dist_name = 'fc' retval['distro_release'] = '%s%s' % (dist_name, retval['relval']) retval['dist'] = retval['dist_release'] = retval['distro_release'] else: r = remote.run( args=['lsb_release', '-sc'], stdout=StringIO(), ) retval['dist'] = r.stdout.getvalue().strip() retval['distro_release'] = None retval['dist_release'] = None # branch/tag/sha1 flavor retval['flavor'] = config.get('flavor', 'basic') log.info('config is %s', config) tag = _get_config_value_for_remote(ctx, remote, config, 'tag') branch = _get_config_value_for_remote(ctx, remote, config, 'branch') sha1 = _get_config_value_for_remote(ctx, remote, config, 'sha1') uri = _get_uri(tag, branch, sha1) retval['uri'] = uri return retval def _get_baseurl(ctx, remote, config): """ Figures out which package repo base URL to use. Example: 'http://gitbuilder.ceph.com/ceph-deb-raring-x86_64-basic/ref/master' :param ctx: the argparse.Namespace object :param remote: the teuthology.orchestra.remote.Remote object :param config: the config dict :returns: str -- the URL """ # get distro name and arch baseparms = _get_baseurlinfo_and_dist(ctx, remote, config) base_url = 'http://{host}/{proj}-{pkg_type}-{dist}-{arch}-{flavor}/{uri}'.format( host=ctx.teuthology_config.get('gitbuilder_host', 'gitbuilder.ceph.com'), proj=config.get('project', 'ceph'), pkg_type=remote.system_type, **baseparms ) return base_url class VersionNotFoundError(Exception): def __init__(self, url): self.url = url def __str__(self): return "Failed to fetch package version from %s" % self.url def _block_looking_for_package_version(remote, base_url, wait=False): """ Look for, and parse, a file called 'version' in base_url. :param remote: the teuthology.orchestra.remote.Remote object :param wait: wait forever for the file to show up. (default False) :returns: str -- the version e.g. '0.67-240-g67a95b9-1raring' :raises: VersionNotFoundError """ while True: r = remote.run( args=['wget', '-q', '-O-', base_url + '/version'], stdout=StringIO(), check_status=False, ) if r.exitstatus != 0: if wait: log.info('Package not there yet, waiting...') time.sleep(15) continue raise VersionNotFoundError(base_url) break version = r.stdout.getvalue().strip() return version def _get_local_dir(config, remote): """ Extract local directory name from the task lists. Copy files over to the remote site. """ ldir = config.get('local', None) if ldir: remote.run(args=['sudo', 'mkdir', '-p', ldir,]) for fyle in os.listdir(ldir): fname = "%s/%s" % (ldir, fyle) teuthology.sudo_write_file(remote, fname, open(fname).read(), '644') return ldir def _update_deb_package_list_and_install(ctx, remote, debs, config): """ Runs ``apt-get update`` first, then runs ``apt-get install``, installing the requested packages on the remote system. TODO: split this into at least two functions. :param ctx: the argparse.Namespace object :param remote: the teuthology.orchestra.remote.Remote object :param debs: list of packages names to install :param config: the config dict """ # check for ceph release key r = remote.run( args=[ 'sudo', 'apt-key', 'list', run.Raw('|'), 'grep', 'Ceph', ], stdout=StringIO(), check_status=False, ) if r.stdout.getvalue().find('Ceph automated package') == -1: # if it doesn't exist, add it remote.run( args=[ 'wget', '-q', '-O-', 'https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/autobuild.asc', run.Raw('|'), 'sudo', 'apt-key', 'add', '-', ], stdout=StringIO(), ) baseparms = _get_baseurlinfo_and_dist(ctx, remote, config) log.info("Installing packages: {pkglist} on remote deb {arch}".format( pkglist=", ".join(debs), arch=baseparms['arch']) ) # get baseurl base_url = _get_baseurl(ctx, remote, config) log.info('Pulling from %s', base_url) # get package version string # FIXME this is a terrible hack. while True: r = remote.run( args=[ 'wget', '-q', '-O-', base_url + '/version', ], stdout=StringIO(), check_status=False, ) if r.exitstatus != 0: if config.get('wait_for_package'): log.info('Package not there yet, waiting...') time.sleep(15) continue raise VersionNotFoundError("%s/version" % base_url) version = r.stdout.getvalue().strip() log.info('Package version is %s', version) break remote.run( args=[ 'echo', 'deb', base_url, baseparms['dist'], 'main', run.Raw('|'), 'sudo', 'tee', '/etc/apt/sources.list.d/{proj}.list'.format( proj=config.get('project', 'ceph')), ], stdout=StringIO(), ) remote.run( args=[ 'sudo', 'apt-get', 'update', run.Raw('&&'), 'sudo', 'DEBIAN_FRONTEND=noninteractive', 'apt-get', '-y', '--force-yes', '-o', run.Raw('Dpkg::Options::="--force-confdef"'), '-o', run.Raw( 'Dpkg::Options::="--force-confold"'), 'install', ] + ['%s=%s' % (d, version) for d in debs], stdout=StringIO(), ) ldir = _get_local_dir(config, remote) if ldir: for fyle in os.listdir(ldir): fname = "%s/%s" % (ldir, fyle) remote.run(args=['sudo', 'dpkg', '-i', fname],) def _yum_fix_repo_priority(remote, project, uri): """ On the remote, 'priority=1' lines to each enabled repo in: /etc/yum.repos.d/{project}.repo :param remote: the teuthology.orchestra.remote.Remote object :param project: the project whose repos need modification """ remote.run( args=[ 'sudo', 'sed', '-i', '-e', run.Raw( '\':a;N;$!ba;s/enabled=1\\ngpg/enabled=1\\npriority=1\\ngpg/g\''), '-e', run.Raw("'s;ref/[a-zA-Z0-9_]*/;{uri}/;g'".format(uri=uri)), '/etc/yum.repos.d/%s.repo' % project, ] ) def _update_rpm_package_list_and_install(ctx, remote, rpm, config): """ Installs the ceph-release package for the relevant branch, then installs the requested packages on the remote system. TODO: split this into at least two functions. :param ctx: the argparse.Namespace object :param remote: the teuthology.orchestra.remote.Remote object :param rpm: list of packages names to install :param config: the config dict """ baseparms = _get_baseurlinfo_and_dist(ctx, remote, config) log.info("Installing packages: {pkglist} on remote rpm {arch}".format( pkglist=", ".join(rpm), arch=baseparms['arch'])) host = ctx.teuthology_config.get('gitbuilder_host', 'gitbuilder.ceph.com') dist_release = baseparms['dist_release'] start_of_url = 'http://{host}/ceph-rpm-{distro_release}-{arch}-{flavor}/{uri}'.format( host=host, **baseparms) ceph_release = 'ceph-release-{release}.{dist_release}.noarch'.format( release=RELEASE, dist_release=dist_release) rpm_name = "{rpm_nm}.rpm".format(rpm_nm=ceph_release) base_url = "{start_of_url}/noarch/{rpm_name}".format( start_of_url=start_of_url, rpm_name=rpm_name) err_mess = StringIO() try: # When this was one command with a pipe, it would sometimes # fail with the message 'rpm: no packages given for install' remote.run(args=['wget', base_url, ],) remote.run(args=['sudo', 'rpm', '-i', rpm_name, ], stderr=err_mess, ) except Exception: cmp_msg = 'package {pkg} is already installed'.format( pkg=ceph_release) if cmp_msg != err_mess.getvalue().strip(): raise remote.run(args=['rm', '-f', rpm_name]) # Fix Repo Priority uri = baseparms['uri'] _yum_fix_repo_priority(remote, config.get('project', 'ceph'), uri) remote.run( args=[ 'sudo', 'yum', 'clean', 'all', ]) version_no = StringIO() version_url = "{start_of_url}/version".format(start_of_url=start_of_url) while True: r = remote.run(args=['wget', '-q', '-O-', version_url, ], stdout=version_no, check_status=False) if r.exitstatus != 0: if config.get('wait_for_package'): log.info('Package not there yet, waiting...') time.sleep(15) continue raise VersionNotFoundError(version_url) version = r.stdout.getvalue().strip() log.info('Package version is %s', version) break tmp_vers = version_no.getvalue().strip()[1:] if '-' in tmp_vers: tmp_vers = tmp_vers.split('-')[0] ldir = _get_local_dir(config, remote) for cpack in rpm: pk_err_mess = StringIO() pkg2add = "{cpack}-{version}".format(cpack=cpack, version=tmp_vers) pkg = None if ldir: pkg = "{ldir}/{cpack}-{trailer}".format(ldir=ldir, cpack=cpack, trailer=tmp_vers) remote.run( args = ['if', 'test', '-e', run.Raw(pkg), run.Raw(';'), 'then', 'sudo', 'yum', 'remove', pkg, '-y', run.Raw(';'), 'sudo', 'yum', 'install', pkg, '-y', run.Raw(';'), 'fi'] ) if pkg is None: remote.run(args=['sudo', 'yum', 'install', pkg2add, '-y', ], stderr=pk_err_mess) else: remote.run( args = ['if', 'test', run.Raw('!'), '-e', run.Raw(pkg), run.Raw(';'), 'then', 'sudo', 'yum', 'install', pkg2add, '-y', run.Raw(';'), 'fi']) def purge_data(ctx): """ Purge /var/lib/ceph on every remote in ctx. :param ctx: the argparse.Namespace object """ with parallel() as p: for remote in ctx.cluster.remotes.iterkeys(): p.spawn(_purge_data, remote) def _purge_data(remote): """ Purge /var/lib/ceph on remote. :param remote: the teuthology.orchestra.remote.Remote object """ log.info('Purging /var/lib/ceph on %s', remote) remote.run(args=[ 'sudo', 'rm', '-rf', '--one-file-system', '--', '/var/lib/ceph', run.Raw('||'), 'true', run.Raw(';'), 'test', '-d', '/var/lib/ceph', run.Raw('&&'), 'sudo', 'find', '/var/lib/ceph', '-mindepth', '1', '-maxdepth', '2', '-type', 'd', '-exec', 'umount', '{}', ';', run.Raw(';'), 'sudo', 'rm', '-rf', '--one-file-system', '--', '/var/lib/ceph', ]) def install_packages(ctx, pkgs, config): """ Installs packages on each remote in ctx. :param ctx: the argparse.Namespace object :param pkgs: list of packages names to install :param config: the config dict """ install_pkgs = { "deb": _update_deb_package_list_and_install, "rpm": _update_rpm_package_list_and_install, } with parallel() as p: for remote in ctx.cluster.remotes.iterkeys(): system_type = teuthology.get_system_type(remote) p.spawn( install_pkgs[system_type], ctx, remote, pkgs[system_type], config) def _remove_deb(ctx, config, remote, debs): """ Removes Debian packages from remote, rudely TODO: be less rude (e.g. using --force-yes) :param ctx: the argparse.Namespace object :param config: the config dict :param remote: the teuthology.orchestra.remote.Remote object :param debs: list of packages names to install """ log.info("Removing packages: {pkglist} on Debian system.".format( pkglist=", ".join(debs))) # first ask nicely remote.run( args=[ 'for', 'd', 'in', ] + debs + [ run.Raw(';'), 'do', 'sudo', 'DEBIAN_FRONTEND=noninteractive', 'apt-get', '-y', '--force-yes', '-o', run.Raw('Dpkg::Options::="--force-confdef"'), '-o', run.Raw( 'Dpkg::Options::="--force-confold"'), 'purge', run.Raw('$d'), run.Raw('||'), 'true', run.Raw(';'), 'done', ]) # mop up anything that is broken remote.run( args=[ 'dpkg', '-l', run.Raw('|'), 'grep', '^.HR', run.Raw('|'), 'awk', '{print $2}', run.Raw('|'), 'sudo', 'xargs', '--no-run-if-empty', 'dpkg', '-P', '--force-remove-reinstreq', ]) # then let apt clean up remote.run( args=[ 'sudo', 'DEBIAN_FRONTEND=noninteractive', 'apt-get', '-y', '--force-yes', '-o', run.Raw('Dpkg::Options::="--force-confdef"'), '-o', run.Raw( 'Dpkg::Options::="--force-confold"'), 'autoremove', ], stdout=StringIO(), ) def _remove_rpm(ctx, config, remote, rpm): """ Removes RPM packages from remote :param ctx: the argparse.Namespace object :param config: the config dict :param remote: the teuthology.orchestra.remote.Remote object :param rpm: list of packages names to remove """ log.info("Removing packages: {pkglist} on rpm system.".format( pkglist=", ".join(rpm))) baseparms = _get_baseurlinfo_and_dist(ctx, remote, config) dist_release = baseparms['dist_release'] remote.run( args=[ 'for', 'd', 'in', ] + rpm + [ run.Raw(';'), 'do', 'sudo', 'yum', 'remove', run.Raw('$d'), '-y', run.Raw('||'), 'true', run.Raw(';'), 'done', ]) remote.run( args=[ 'sudo', 'yum', 'clean', 'all', ]) projRelease = '%s-release-%s.%s.noarch' % ( config.get('project', 'ceph'), RELEASE, dist_release) remote.run(args=['sudo', 'yum', 'erase', projRelease, '-y']) remote.run( args=[ 'sudo', 'yum', 'clean', 'expire-cache', ]) def remove_packages(ctx, config, pkgs): """ Removes packages from each remote in ctx. :param ctx: the argparse.Namespace object :param config: the config dict :param pkgs: list of packages names to remove """ remove_pkgs = { "deb": _remove_deb, "rpm": _remove_rpm, } with parallel() as p: for remote in ctx.cluster.remotes.iterkeys(): system_type = teuthology.get_system_type(remote) p.spawn(remove_pkgs[ system_type], ctx, config, remote, pkgs[system_type]) def _remove_sources_list_deb(remote, proj): """ Removes /etc/apt/sources.list.d/{proj}.list and then runs ``apt-get update``. :param remote: the teuthology.orchestra.remote.Remote object :param proj: the project whose sources.list needs removing """ remote.run( args=[ 'sudo', 'rm', '-f', '/etc/apt/sources.list.d/{proj}.list'.format( proj=proj), run.Raw('&&'), 'sudo', 'apt-get', 'update', # ignore failure run.Raw('||'), 'true', ], stdout=StringIO(), ) def _remove_sources_list_rpm(remote, proj): """ Removes /etc/yum.repos.d/{proj}.repo, /var/lib/{proj}, and /var/log/{proj}. :param remote: the teuthology.orchestra.remote.Remote object :param proj: the project whose sources.list needs removing """ remote.run( args=[ 'sudo', 'rm', '-f', '/etc/yum.repos.d/{proj}.repo'.format( proj=proj), run.Raw('||'), 'true', ], stdout=StringIO(), ) # FIXME # There probably should be a way of removing these files that is # implemented in the yum/rpm remove procedures for the ceph package. # FIXME but why is this function doing these things? remote.run( args=[ 'sudo', 'rm', '-fr', '/var/lib/{proj}'.format(proj=proj), run.Raw('||'), 'true', ], stdout=StringIO(), ) remote.run( args=[ 'sudo', 'rm', '-fr', '/var/log/{proj}'.format(proj=proj), run.Raw('||'), 'true', ], stdout=StringIO(), ) def remove_sources(ctx, config): """ Removes repo source files from each remote in ctx. :param ctx: the argparse.Namespace object :param config: the config dict """ remove_sources_pkgs = { 'deb': _remove_sources_list_deb, 'rpm': _remove_sources_list_rpm, } log.info("Removing {proj} sources lists".format( proj=config.get('project', 'ceph'))) with parallel() as p: for remote in ctx.cluster.remotes.iterkeys(): system_type = teuthology.get_system_type(remote) p.spawn(remove_sources_pkgs[ system_type], remote, config.get('project', 'ceph')) p.spawn(remove_sources_pkgs[ system_type], remote, 'calamari') deb_packages = {'ceph': [ 'ceph', 'ceph-dbg', 'ceph-mds', 'ceph-mds-dbg', 'ceph-common', 'ceph-common-dbg', 'ceph-fuse', 'ceph-fuse-dbg', 'ceph-test', 'ceph-test-dbg', 'radosgw', 'radosgw-dbg', 'python-ceph', 'libcephfs1', 'libcephfs1-dbg', ]} rpm_packages = {'ceph': [ 'ceph-debuginfo', 'ceph-radosgw', 'ceph-test', 'ceph-devel', 'ceph', 'ceph-fuse', 'rest-bench', 'libcephfs_jni1', 'libcephfs1', 'python-ceph', ]} @contextlib.contextmanager def install(ctx, config): """ The install task. Installs packages for a given project on all hosts in ctx. May work for projects besides ceph, but may not. Patches welcomed! :param ctx: the argparse.Namespace object :param config: the config dict """ project = config.get('project', 'ceph') global deb_packages global rpm_packages debs = deb_packages.get(project, []) rpm = rpm_packages.get(project, []) # pull any additional packages out of config extra_pkgs = config.get('extra_packages') log.info('extra packages: {packages}'.format(packages=extra_pkgs)) debs += extra_pkgs rpm += extra_pkgs # the extras option right now is specific to the 'ceph' project extras = config.get('extras') if extras is not None: debs = ['ceph-test', 'ceph-test-dbg', 'ceph-fuse', 'ceph-fuse-dbg', 'librados2', 'librados2-dbg', 'librbd1', 'librbd1-dbg', 'python-ceph'] rpm = ['ceph-fuse', 'librbd1', 'librados2', 'ceph-test', 'python-ceph'] # install lib deps (so we explicitly specify version), but do not # uninstall them, as other packages depend on them (e.g., kvm) proj_install_debs = {'ceph': [ 'librados2', 'librados2-dbg', 'librbd1', 'librbd1-dbg', ]} proj_install_rpm = {'ceph': [ 'librbd1', 'librados2', ]} install_debs = proj_install_debs.get(project, []) install_rpm = proj_install_rpm.get(project, []) install_info = { "deb": debs + install_debs, "rpm": rpm + install_rpm} remove_info = { "deb": debs, "rpm": rpm} install_packages(ctx, install_info, config) try: yield finally: remove_packages(ctx, config, remove_info) remove_sources(ctx, config) if project == 'ceph': purge_data(ctx) def _upgrade_deb_packages(ctx, config, remote, debs): """ Upgrade project's packages on remote Debian host Before doing so, installs the project's GPG key, writes a sources.list file, and runs ``apt-get update``. :param ctx: the argparse.Namespace object :param config: the config dict :param remote: the teuthology.orchestra.remote.Remote object :param debs: the Debian packages to be installed :param branch: the branch of the project to be used """ # check for ceph release key r = remote.run( args=[ 'sudo', 'apt-key', 'list', run.Raw('|'), 'grep', 'Ceph', ], stdout=StringIO(), check_status=False, ) if r.stdout.getvalue().find('Ceph automated package') == -1: # if it doesn't exist, add it remote.run( args=[ 'wget', '-q', '-O-', 'https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/autobuild.asc', run.Raw('|'), 'sudo', 'apt-key', 'add', '-', ], stdout=StringIO(), ) # get distro name and arch r = remote.run( args=['lsb_release', '-sc'], stdout=StringIO(), ) dist = r.stdout.getvalue().strip() r = remote.run( args=['arch'], stdout=StringIO(), ) arch = r.stdout.getvalue().strip() log.info("dist %s arch %s", dist, arch) # branch/tag/sha1 flavor flavor = 'basic' sha1 = config.get('sha1') branch = config.get('branch') tag = config.get('tag') uri = _get_uri(tag, branch, sha1) base_url = 'http://{host}/{proj}-deb-{dist}-{arch}-{flavor}/{uri}'.format( host=ctx.teuthology_config.get('gitbuilder_host', 'gitbuilder.ceph.com'), proj=config.get('project', 'ceph'), dist=dist, arch=arch, flavor=flavor, uri=uri, ) log.info('Pulling from %s', base_url) # get package version string while True: r = remote.run( args=[ 'wget', '-q', '-O-', base_url + '/version', ], stdout=StringIO(), check_status=False, ) if r.exitstatus != 0: if config.get('wait_for_package'): log.info('Package not there yet, waiting...') time.sleep(15) continue raise VersionNotFoundError("%s/version" % base_url) version = r.stdout.getvalue().strip() log.info('Package version is %s', version) break remote.run( args=[ 'echo', 'deb', base_url, dist, 'main', run.Raw('|'), 'sudo', 'tee', '/etc/apt/sources.list.d/{proj}.list'.format( proj=config.get('project', 'ceph')), ], stdout=StringIO(), ) remote.run( args=[ 'sudo', 'apt-get', 'update', run.Raw('&&'), 'sudo', 'DEBIAN_FRONTEND=noninteractive', 'apt-get', '-y', '--force-yes', '-o', run.Raw('Dpkg::Options::="--force-confdef"'), '-o', run.Raw( 'Dpkg::Options::="--force-confold"'), 'install', ] + ['%s=%s' % (d, version) for d in debs], stdout=StringIO(), ) def _upgrade_rpm_packages(ctx, config, remote, pkgs): """ Upgrade project's packages on remote RPM-based host Before doing so, it makes sure the project's -release RPM is installed - removing any previous version first. :param ctx: the argparse.Namespace object :param config: the config dict :param remote: the teuthology.orchestra.remote.Remote object :param pkgs: the RPM packages to be installed :param branch: the branch of the project to be used """ distinfo = _get_baseurlinfo_and_dist(ctx, remote, config) log.info( "Host {host} is: {distro} {ver} {arch}".format( host=remote.shortname, distro=distinfo['distro'], ver=distinfo['relval'], arch=distinfo['arch'],) ) base_url = _get_baseurl(ctx, remote, config) log.info('Repo base URL: %s', base_url) version = _block_looking_for_package_version( remote, base_url, config.get('wait_for_package', False)) # FIXME: 'version' as retreived from the repo is actually the RPM version # PLUS *part* of the release. Example: # Right now, ceph master is given the following version in the repo file: # v0.67-rc3.164.gd5aa3a9 - whereas in reality the RPM version is 0.61.7 # and the release is 37.g1243c97.el6 (for centos6). # Point being, I have to mangle a little here. if version[0] == 'v': version = version[1:] if '-' in version: version = version.split('-')[0] project = config.get('project', 'ceph') # Remove the -release package before upgrading it args = ['sudo', 'rpm', '-ev', '%s-release' % project] _run_and_log_error_if_fails(remote, args) # Build the new -release package path release_rpm = "{base}/noarch/{proj}-release-{release}.{dist_release}.noarch.rpm".format( base=base_url, proj=project, release=RELEASE, dist_release=distinfo['dist_release'], ) # Upgrade the -release package args = ['sudo', 'rpm', '-Uv', release_rpm] _run_and_log_error_if_fails(remote, args) uri = _get_baseurlinfo_and_dist(ctx, remote, config)['uri'] _yum_fix_repo_priority(remote, project, uri) remote.run( args=[ 'sudo', 'yum', 'clean', 'all', ]) # Build a space-separated string consisting of $PKG-$VER for yum pkgs_with_vers = ["%s-%s" % (pkg, version) for pkg in pkgs] # Actually upgrade the project packages # FIXME: This currently outputs nothing until the command is finished # executing. That sucks; fix it. args = ['sudo', 'yum', '-y', 'install'] args += pkgs_with_vers _run_and_log_error_if_fails(remote, args) @contextlib.contextmanager def upgrade(ctx, config): """ Upgrades packages for a given project. For example:: tasks: - install.upgrade: all: branch: end or specify specific roles:: tasks: - install.upgrade: mon.a: branch: end osd.0: branch: other or rely on the overrides for the target version:: overrides: install: ceph: sha1: ... tasks: - install.upgrade: all: (HACK: the overrides will *only* apply the sha1/branch/tag if those keys are not present in the config.) :param ctx: the argparse.Namespace object :param config: the config dict """ assert config is None or isinstance(config, dict), \ "install.upgrade only supports a dictionary for configuration" for i in config.keys(): assert config.get(i) is None or isinstance( config.get(i), dict), 'host supports dictionary' project = config.get('project', 'ceph') # use 'install' overrides here, in case the upgrade target is left # unspecified/implicit. install_overrides = ctx.config.get( 'overrides', {}).get('install', {}).get(project, {}) log.info('project %s config %s overrides %s', project, config, install_overrides) # FIXME: extra_pkgs is not distro-agnostic extra_pkgs = config.get('extra_packages', []) log.info('extra packages: {packages}'.format(packages=extra_pkgs)) # build a normalized remote -> config dict remotes = {} if 'all' in config: for remote in ctx.cluster.remotes.iterkeys(): remotes[remote] = config.get('all') else: for role in config.keys(): (remote,) = ctx.cluster.only(role).remotes.iterkeys() if remote in remotes: log.warn('remote %s came up twice (role %s)', remote, role) continue remotes[remote] = config.get(role) for remote, node in remotes.iteritems(): if not node: node = {} this_overrides = copy.deepcopy(install_overrides) if 'sha1' in node or 'tag' in node or 'branch' in node: log.info('config contains sha1|tag|branch, removing those keys from override') this_overrides.pop('sha1', None) this_overrides.pop('tag', None) this_overrides.pop('branch', None) teuthology.deep_merge(node, this_overrides) log.info('remote %s config %s', remote, node) system_type = teuthology.get_system_type(remote) assert system_type in ('deb', 'rpm') pkgs = PACKAGES[project][system_type] log.info("Upgrading {proj} {system_type} packages: {pkgs}".format( proj=project, system_type=system_type, pkgs=', '.join(pkgs))) # FIXME: again, make extra_pkgs distro-agnostic pkgs += extra_pkgs node['project'] = project if system_type == 'deb': _upgrade_deb_packages(ctx, node, remote, pkgs) elif system_type == 'rpm': _upgrade_rpm_packages(ctx, node, remote, pkgs) yield @contextlib.contextmanager def task(ctx, config): """ Install packages for a given project. tasks: - install: project: ceph branch: bar - install: project: samba branch: foo extra_packages: ['samba'] Overrides are project specific: overrides: install: ceph: sha1: ... :param ctx: the argparse.Namespace object :param config: the config dict """ if config is None: config = {} assert isinstance(config, dict), \ "task install only supports a dictionary for configuration" project, = config.get('project', 'ceph'), log.debug('project %s' % project) overrides = ctx.config.get('overrides') if overrides: install_overrides = overrides.get('install', {}) teuthology.deep_merge(config, install_overrides.get(project, {})) log.debug('config %s' % config) # Flavor tells us what gitbuilder to fetch the prebuilt software # from. It's a combination of possible keywords, in a specific # order, joined by dashes. It is used as a URL path name. If a # match is not found, the teuthology run fails. This is ugly, # and should be cleaned up at some point. flavor = config.get('flavor', 'basic') if config.get('path'): # local dir precludes any other flavors flavor = 'local' else: if config.get('valgrind'): log.info( 'Using notcmalloc flavor and running some daemons under valgrind') flavor = 'notcmalloc' else: if config.get('coverage'): log.info('Recording coverage for this run.') flavor = 'gcov' ctx.summary['flavor'] = flavor with contextutil.nested( lambda: install(ctx=ctx, config=dict( branch=config.get('branch'), tag=config.get('tag'), sha1=config.get('sha1'), flavor=flavor, extra_packages=config.get('extra_packages', []), extras=config.get('extras', None), wait_for_package=ctx.config.get('wait_for_package', False), project=project, )), ): yield