Blame tools/read-dependencies

Packit Service 751c4a
#!/usr/bin/env python3
Packit Service a04d08
"""List pip dependencies or system package dependencies for cloud-init."""
Packit Service a04d08
Packit Service a04d08
# You might be tempted to rewrite this as a shell script, but you
Packit Service a04d08
# would be surprised to discover that things like 'egrep' or 'sed' may
Packit Service a04d08
# differ between Linux and *BSD.
Packit Service a04d08
Packit Service a04d08
try:
Packit Service a04d08
    from argparse import ArgumentParser
Packit Service a04d08
except ImportError:
Packit Service a04d08
    raise RuntimeError(
Packit Service 751c4a
        'Could not import argparse. Please install python3-argparse '
Packit Service a04d08
        'package to continue')
Packit Service a04d08
Packit Service a04d08
import json
Packit Service a04d08
import os
Packit Service a04d08
import re
Packit Service a04d08
import subprocess
Packit Service a04d08
import sys
Packit Service a04d08
Packit Service a04d08
DEFAULT_REQUIREMENTS = 'requirements.txt'
Packit Service a04d08
Packit Service a04d08
# Map the appropriate package dir needed for each distro choice
Packit Service a04d08
DISTRO_PKG_TYPE_MAP = {
Packit Service a04d08
    'centos': 'redhat',
Packit Service a04d08
    'redhat': 'redhat',
Packit Service a04d08
    'debian': 'debian',
Packit Service a04d08
    'ubuntu': 'debian',
Packit Service a04d08
    'opensuse': 'suse',
Packit Service a04d08
    'suse': 'suse'
Packit Service a04d08
}
Packit Service a04d08
Packit Service a04d08
MAYBE_RELIABLE_YUM_INSTALL = [
Packit Service a04d08
    'sh', '-c',
Packit Service a04d08
    """
Packit Service a04d08
    error() { echo "$@" 1>&2; }
Packit Service 751c4a
    configure_repos_for_proxy_use() {
Packit Service 751c4a
        grep -q "^proxy=" /etc/yum.conf || return 0
Packit Service 751c4a
        error ":: http proxy in use => forcing the use of fixed URLs in /etc/yum.repos.d/*.repo"
Packit Service 751c4a
        sed -i --regexp-extended '/^#baseurl=/s/#// ; /^(mirrorlist|metalink)=/s/^/#/' /etc/yum.repos.d/*.repo
Packit Service 751c4a
        sed -i 's/download\.fedoraproject\.org/dl.fedoraproject.org/g' /etc/yum.repos.d/*.repo
Packit Service 751c4a
    }
Packit Service 751c4a
    configure_repos_for_proxy_use
Packit Service a04d08
    n=0; max=10;
Packit Service a04d08
    bcmd="yum install --downloadonly --assumeyes --setopt=keepcache=1"
Packit Service a04d08
    while n=$(($n+1)); do
Packit Service a04d08
       error ":: running $bcmd $* [$n/$max]"
Packit Service a04d08
       $bcmd "$@"
Packit Service a04d08
       r=$?
Packit Service a04d08
       [ $r -eq 0 ] && break
Packit Service a04d08
       [ $n -ge $max ] && { error "gave up on $bcmd"; exit $r; }
Packit Service a04d08
       nap=$(($n*5))
Packit Service a04d08
       error ":: failed [$r] ($n/$max). sleeping $nap."
Packit Service a04d08
       sleep $nap
Packit Service a04d08
    done
Packit Service a04d08
    error ":: running yum install --cacheonly --assumeyes $*"
Packit Service a04d08
    yum install --cacheonly --assumeyes "$@"
Packit Service 751c4a
    configure_repos_for_proxy_use
Packit Service a04d08
    """,
Packit Service a04d08
    'reliable-yum-install']
Packit Service a04d08
Packit Service a04d08
ZYPPER_INSTALL = [
Packit Service a04d08
    'zypper', '--non-interactive', '--gpg-auto-import-keys', 'install',
Packit Service a04d08
    '--auto-agree-with-licenses']
Packit Service a04d08
Packit Service a04d08
DRY_DISTRO_INSTALL_PKG_CMD = {
Packit Service a04d08
    'centos': ['yum', 'install', '--assumeyes'],
Packit Service a04d08
    'redhat': ['yum', 'install', '--assumeyes'],
Packit Service a04d08
}
Packit Service a04d08
Packit Service a04d08
DISTRO_INSTALL_PKG_CMD = {
Packit Service a04d08
    'centos': MAYBE_RELIABLE_YUM_INSTALL,
Packit Service a04d08
    'redhat': MAYBE_RELIABLE_YUM_INSTALL,
Packit Service a04d08
    'debian': ['apt', 'install', '-y'],
Packit Service a04d08
    'ubuntu': ['apt', 'install', '-y'],
Packit Service a04d08
    'opensuse': ZYPPER_INSTALL,
Packit Service a04d08
    'suse': ZYPPER_INSTALL,
Packit Service a04d08
}
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# List of base system packages required to enable ci automation
Packit Service a04d08
CI_SYSTEM_BASE_PKGS = {
Packit Service a04d08
    'common': ['make', 'sudo', 'tar'],
Packit Service 751c4a
    'redhat': ['python3-tox'],
Packit Service 751c4a
    'centos': ['python3-tox'],
Packit Service a04d08
    'ubuntu': ['devscripts', 'python3-dev', 'libssl-dev', 'tox', 'sbuild'],
Packit Service a04d08
    'debian': ['devscripts', 'python3-dev', 'libssl-dev', 'tox', 'sbuild']}
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# JSON definition of distro-specific package dependencies
Packit Service a04d08
DISTRO_PKG_DEPS_PATH = "packages/pkg-deps.json"
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def get_parser():
Packit Service a04d08
    """Return an argument parser for this command."""
Packit Service a04d08
    parser = ArgumentParser(description=__doc__)
Packit Service a04d08
    parser.add_argument(
Packit Service a04d08
        '-r', '--requirements-file', type=str, dest='req_files',
Packit Service a04d08
        action='append', default=None,
Packit Service a04d08
        help='pip-style requirements file [default=%s]' % DEFAULT_REQUIREMENTS)
Packit Service a04d08
    parser.add_argument(
Packit Service a04d08
        '-d', '--distro', type=str, choices=DISTRO_PKG_TYPE_MAP.keys(),
Packit Service a04d08
        help='The name of the distro to generate package deps for.')
Packit Service 751c4a
    deptype = parser.add_mutually_exclusive_group()
Packit Service 751c4a
    deptype.add_argument(
Packit Service 751c4a
        '-R', '--runtime-requires', action='store_true', default=False,
Packit Service 751c4a
        dest='runtime_requires',
Packit Service 751c4a
        help='Print only runtime required packages')
Packit Service 751c4a
    deptype.add_argument(
Packit Service 751c4a
        '-b', '--build-requires', action='store_true', default=False,
Packit Service 751c4a
        dest='build_requires', help='Print only buildtime required packages')
Packit Service a04d08
    parser.add_argument(
Packit Service a04d08
        '--dry-run', action='store_true', default=False, dest='dry_run',
Packit Service a04d08
        help='Dry run the install, making no package changes.')
Packit Service a04d08
    parser.add_argument(
Packit Service a04d08
        '-s', '--system-pkg-names', action='store_true', default=False,
Packit Service a04d08
        dest='system_pkg_names',
Packit Service 751c4a
        help='Generate distribution package names (python3-pkgname).')
Packit Service a04d08
    parser.add_argument(
Packit Service a04d08
        '-i', '--install', action='store_true', default=False,
Packit Service a04d08
        dest='install',
Packit Service a04d08
        help='When specified, install the required system packages.')
Packit Service a04d08
    parser.add_argument(
Packit Service a04d08
        '-t', '--test-distro', action='store_true', default=False,
Packit Service a04d08
        dest='test_distro',
Packit Service a04d08
        help='Additionally install continuous integration system packages '
Packit Service a04d08
             'required for build and test automation.')
Packit Service a04d08
    return parser
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def get_package_deps_from_json(topdir, distro):
Packit Service a04d08
    """Get a dict of build and runtime package requirements for a distro.
Packit Service a04d08
Packit Service a04d08
    @param topdir: The root directory in which to search for the
Packit Service a04d08
        DISTRO_PKG_DEPS_PATH json blob of package requirements information.
Packit Service a04d08
    @param distro: The specific distribution shortname to pull dependencies
Packit Service a04d08
        for.
Packit Service a04d08
    @return: Dict containing "requires", "build-requires" and "rename" lists
Packit Service a04d08
        for a given distribution.
Packit Service a04d08
    """
Packit Service a04d08
    with open(os.path.join(topdir, DISTRO_PKG_DEPS_PATH), 'r') as stream:
Packit Service a04d08
        deps = json.loads(stream.read())
Packit Service a04d08
    if distro is None:
Packit Service a04d08
        return {}
Packit Service 751c4a
    if deps.get(distro):  # If we have a specific distro defined, use it.
Packit Service 751c4a
        return deps[distro]
Packit Service 751c4a
    # Use generic distro dependency map via DISTRO_PKG_TYPE_MAP
Packit Service a04d08
    return deps[DISTRO_PKG_TYPE_MAP[distro]]
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def parse_pip_requirements(requirements_path):
Packit Service a04d08
    """Return the pip requirement names from pip-style requirements_path."""
Packit Service a04d08
    dep_names = []
Packit Service a04d08
    with open(requirements_path, "r") as fp:
Packit Service a04d08
        for line in fp:
Packit Service a04d08
            line = line.strip()
Packit Service a04d08
            if not line or line.startswith("#"):
Packit Service a04d08
                continue
Packit Service a04d08
Packit Service a04d08
            # remove pip-style markers
Packit Service a04d08
            dep = line.split(';')[0]
Packit Service a04d08
Packit Service a04d08
            # remove version requirements
Packit Service a04d08
            if re.search('[>=.<]+', dep):
Packit Service a04d08
                dep_names.append(re.split(r'[>=.<]+', dep)[0].strip())
Packit Service a04d08
            else:
Packit Service a04d08
                dep_names.append(dep)
Packit Service a04d08
    return dep_names
Packit Service a04d08
Packit Service a04d08
Packit Service 751c4a
def translate_pip_to_system_pkg(pip_requires, renames):
Packit Service a04d08
    """Translate pip package names to distro-specific package names.
Packit Service a04d08
Packit Service a04d08
    @param pip_requires: List of versionless pip package names to translate.
Packit Service a04d08
    @param renames: Dict containg special case renames from pip name to system
Packit Service a04d08
        package name for the distro.
Packit Service a04d08
    """
Packit Service 751c4a
    prefix = "python3-"
Packit Service a04d08
    standard_pkg_name = "{0}{1}"
Packit Service a04d08
    translated_names = []
Packit Service a04d08
    for pip_name in pip_requires:
Packit Service a04d08
        pip_name = pip_name.lower()
Packit Service a04d08
        # Find a rename if present for the distro package and python version
Packit Service 751c4a
        rename = renames.get(pip_name, "")
Packit Service a04d08
        if rename:
Packit Service a04d08
            translated_names.append(rename)
Packit Service a04d08
        else:
Packit Service a04d08
            translated_names.append(
Packit Service a04d08
                standard_pkg_name.format(prefix, pip_name))
Packit Service a04d08
    return translated_names
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def main(distro):
Packit Service a04d08
    parser = get_parser()
Packit Service a04d08
    args = parser.parse_args()
Packit Service a04d08
    if 'CLOUD_INIT_TOP_D' in os.environ:
Packit Service a04d08
        topd = os.path.realpath(os.environ.get('CLOUD_INIT_TOP_D'))
Packit Service a04d08
    else:
Packit Service a04d08
        topd = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
Packit Service a04d08
Packit Service a04d08
    if args.test_distro:
Packit Service a04d08
        # Give us all the system deps we need for continuous integration
Packit Service a04d08
        if args.req_files:
Packit Service a04d08
            sys.stderr.write(
Packit Service a04d08
                "Parameter --test-distro overrides --requirements-file. Use "
Packit Service a04d08
                "one or the other.\n")
Packit Service a04d08
            sys.exit(1)
Packit Service a04d08
        args.req_files = [os.path.join(topd, DEFAULT_REQUIREMENTS),
Packit Service a04d08
                          os.path.join(topd, 'test-' + DEFAULT_REQUIREMENTS)]
Packit Service a04d08
        args.install = True
Packit Service a04d08
    if args.req_files is None:
Packit Service a04d08
        args.req_files = [os.path.join(topd, DEFAULT_REQUIREMENTS)]
Packit Service a04d08
        if not os.path.isfile(args.req_files[0]):
Packit Service a04d08
            sys.stderr.write("Unable to locate '%s' file that should "
Packit Service a04d08
                             "exist in cloud-init root directory." %
Packit Service a04d08
                             args.req_files[0])
Packit Service a04d08
            sys.exit(1)
Packit Service a04d08
Packit Service a04d08
    bad_files = [r for r in args.req_files if not os.path.isfile(r)]
Packit Service a04d08
    if bad_files:
Packit Service a04d08
        sys.stderr.write(
Packit Service a04d08
            "Unable to find requirements files: %s\n" % ','.join(bad_files))
Packit Service a04d08
        sys.exit(1)
Packit Service a04d08
Packit Service a04d08
    pip_pkg_names = set()
Packit Service a04d08
    for req_path in args.req_files:
Packit Service a04d08
        pip_pkg_names.update(set(parse_pip_requirements(req_path)))
Packit Service a04d08
    deps_from_json = get_package_deps_from_json(topd, args.distro)
Packit Service a04d08
    renames = deps_from_json.get('renames', {})
Packit Service a04d08
    translated_pip_names = translate_pip_to_system_pkg(
Packit Service 751c4a
        pip_pkg_names, renames)
Packit Service a04d08
    all_deps = []
Packit Service 751c4a
    select_requires = [args.build_requires, args.runtime_requires]
Packit Service a04d08
    if args.distro:
Packit Service 751c4a
        if not any(select_requires):
Packit Service 751c4a
            all_deps.extend(
Packit Service 751c4a
                translated_pip_names + deps_from_json['requires'] +
Packit Service 751c4a
                deps_from_json['build-requires'])
Packit Service 751c4a
        else:
Packit Service 751c4a
            if args.build_requires:
Packit Service 751c4a
                all_deps.extend(deps_from_json['build-requires'])
Packit Service 751c4a
            else:
Packit Service 751c4a
                all_deps.extend(
Packit Service 751c4a
                    translated_pip_names + deps_from_json['requires'])
Packit Service a04d08
    else:
Packit Service a04d08
        if args.system_pkg_names:
Packit Service a04d08
            all_deps = translated_pip_names
Packit Service a04d08
        else:
Packit Service a04d08
            all_deps = pip_pkg_names
Packit Service 751c4a
    all_deps = sorted(all_deps)
Packit Service a04d08
    if args.install:
Packit Service a04d08
        pkg_install(all_deps, args.distro, args.test_distro, args.dry_run)
Packit Service a04d08
    else:
Packit Service a04d08
        print('\n'.join(all_deps))
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def pkg_install(pkg_list, distro, test_distro=False, dry_run=False):
Packit Service a04d08
    """Install a list of packages using the DISTRO_INSTALL_PKG_CMD."""
Packit Service a04d08
    if test_distro:
Packit Service a04d08
        pkg_list = list(pkg_list) + CI_SYSTEM_BASE_PKGS['common']
Packit Service a04d08
        distro_base_pkgs = CI_SYSTEM_BASE_PKGS.get(distro, [])
Packit Service a04d08
        pkg_list += distro_base_pkgs
Packit Service a04d08
    print('Installing deps: {0}{1}'.format(
Packit Service a04d08
          '(dryrun)' if dry_run else '', ' '.join(pkg_list)))
Packit Service a04d08
    install_cmd = []
Packit Service a04d08
    if dry_run:
Packit Service a04d08
        install_cmd.append('echo')
Packit Service a04d08
    if os.geteuid() != 0:
Packit Service a04d08
        install_cmd.append('sudo')
Packit Service a04d08
Packit Service a04d08
    cmd = DISTRO_INSTALL_PKG_CMD[distro]
Packit Service a04d08
    if dry_run and distro in DRY_DISTRO_INSTALL_PKG_CMD:
Packit Service a04d08
        cmd = DRY_DISTRO_INSTALL_PKG_CMD[distro]
Packit Service a04d08
    install_cmd.extend(cmd)
Packit Service a04d08
Packit Service a04d08
    if distro in ['centos', 'redhat']:
Packit Service a04d08
        # CentOS and Redhat need epel-release to access oauthlib and jsonschema
Packit Service a04d08
        subprocess.check_call(install_cmd + ['epel-release'])
Packit Service a04d08
    if distro in ['suse', 'opensuse', 'redhat', 'centos']:
Packit Service a04d08
        pkg_list.append('rpm-build')
Packit Service a04d08
    subprocess.check_call(install_cmd + pkg_list)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
if __name__ == "__main__":
Packit Service a04d08
    parser = get_parser()
Packit Service a04d08
    args = parser.parse_args()
Packit Service a04d08
    sys.exit(main(args.distro))
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab