#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2018-present ScyllaDB # # # SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0 # import argparse import os import subprocess import tarfile import pathlib import shutil import sys import tempfile import magic from tempfile import mkstemp RELOC_PREFIX='scylla' def reloc_add(self, name, arcname=None, recursive=True, *, filter=None): if arcname: return self.add(name, arcname="{}/{}".format(RELOC_PREFIX, arcname), filter=filter) else: return self.add(name, arcname="{}/{}".format(RELOC_PREFIX, name), filter=filter) tarfile.TarFile.reloc_add = reloc_add def ldd(executable): '''Given an executable file, return a dictionary with the keys containing its shared library dependencies and the values pointing at the files they resolve to. A fake key ld.so points at the dynamic loader.''' libraries = {} for ldd_line in subprocess.check_output( ['ldd', executable], universal_newlines=True).splitlines(): elements = ldd_line.split() if ldd_line.endswith('not found'): raise Exception('ldd {}: could not resolve {}'.format(executable, elements[0])) if elements[1] != '=>': if elements[0].startswith('linux-vdso.so'): # provided by kernel continue libraries['ld.so'] = os.path.realpath(elements[0]) elif '//' in elements[0]: # We know that the only DSO with a // in the path is the # dynamic linker used by scylla, which is the same ld.so # above. pass else: libraries[elements[0]] = os.path.realpath(elements[2]) hmacfile = elements[2].replace('/lib64/', '/lib64/.') + '.hmac' if os.path.exists(hmacfile): arcname = os.path.basename(hmacfile) libraries[arcname] = os.path.realpath(hmacfile) fc_hmacfile = elements[2].replace('/lib64/', '/lib64/fipscheck/') + '.hmac' if os.path.exists(fc_hmacfile): arcname = 'fipscheck/' + os.path.basename(fc_hmacfile) libraries[arcname] = os.path.realpath(fc_hmacfile) return libraries def filter_dist(info): for x in ['dist/ami/files/', 'dist/ami/packer', 'dist/ami/variables.json']: if info.name.startswith(x): return None return info SCYLLA_DIR='scylla-package' def reloc_add(ar, name, arcname=None): ar.add(name, arcname="{}/{}".format(SCYLLA_DIR, arcname if arcname else name)) def fipshmac(f): DIRECTORY='build' bn = os.path.basename(f) subprocess.run(['fipshmac', '-d', DIRECTORY, f], check=True) return f'{DIRECTORY}/{bn}.hmac' def fix_hmac(ar, binpath, targetpath, patched_binary): bn = os.path.basename(binpath) dn = os.path.dirname(binpath) targetpath_bn = os.path.basename(targetpath) targetpath_dn = os.path.dirname(targetpath) hmac = f'{dn}/.{bn}.hmac' if os.path.exists(hmac): hmac = fipshmac(patched_binary) hmac_arcname = f'{targetpath_dn}/.{targetpath_bn}.hmac' ar.reloc_add(hmac, arcname=hmac_arcname) fc_hmac = f'{dn}/fipscheck/{bn}.hmac' if os.path.exists(fc_hmac): fc_hmac = fipshmac(patched_binary) fc_hmac_arcname = f'{targetpath_dn}/fipscheck/{targetpath_bn}.hmac' ar.reloc_add(fc_hmac, arcname=fc_hmac_arcname) def fix_binary(ar, path): # it's a pity patchelf have to patch an actual binary. patched_elf = mkstemp()[1] shutil.copy2(path, patched_elf) subprocess.check_call(['patchelf', '--remove-rpath', patched_elf]) return patched_elf def fix_executable(ar, binpath, targetpath): patched_binary = fix_binary(ar, binpath) ar.reloc_add(patched_binary, arcname=targetpath) os.remove(patched_binary) def fix_sharedlib(ar, binpath, targetpath): patched_binary = fix_binary(ar, binpath) ar.reloc_add(patched_binary, arcname=targetpath) fix_hmac(ar, binpath, targetpath, patched_binary) os.remove(patched_binary) ap = argparse.ArgumentParser(description='Create a relocatable scylla package.') ap.add_argument('dest', help='Destination file (tar format)') ap.add_argument('--build-dir', default='build/release', help='Build dir ("build/debug" or "build/release") to use') ap.add_argument('--node-exporter-dir', default='build/node_exporter', help='the directory where node_exporter is located') ap.add_argument('--debian-dir', default='build/debian/debian', help='the directory where debian packaging is located') ap.add_argument('--stripped', action='store_true', help='use stripped binaries') ap.add_argument('--print-libexec', action='store_true', help='print libexec executables and exit script') args = ap.parse_args() executables_scylla = [ '{}/scylla'.format(args.build_dir), '{}/iotune'.format(args.build_dir), '{}/patchelf'.format(args.build_dir), ] executables_distrocmd = [ '/usr/bin/lscpu', '/usr/bin/gawk', '/usr/bin/gzip', '/usr/sbin/ifconfig', '/usr/sbin/ethtool', '/usr/bin/netstat', '/usr/bin/hwloc-distrib', '/usr/bin/hwloc-calc', '/usr/bin/lsblk'] executables = executables_scylla + executables_distrocmd if args.print_libexec: for exec in executables: print(f'libexec/{os.path.basename(exec)}') sys.exit(0) output = args.dest libs = {} for exe in executables: libs.update(ldd(exe)) # manually add libthread_db for debugging thread libs.update({'libthread_db.so.1': os.path.realpath('/lib64/libthread_db.so')}) # manually add p11-kit-trust.so since it will dynamically load libs.update({'pkcs11/p11-kit-trust.so': '/lib64/pkcs11/p11-kit-trust.so'}) ld_so = libs['ld.so'] have_gnutls = any([lib.startswith('libgnutls.so') for lib in libs.keys()]) # Although tarfile.open() can write directly to a compressed tar by using # the "w|gz" mode, it does so using a slow Python implementation. It is as # much as 3 times faster (!) to output to a pipe running the external gzip # command. We can complete the compression even faster by using the pigz # command - a parallel implementation of gzip utilizing all processors # instead of just one. gzip_process = subprocess.Popen("pigz > "+output, shell=True, stdin=subprocess.PIPE) ar = tarfile.open(fileobj=gzip_process.stdin, mode='w|') # relocatable package format version = 3.0 with tempfile.NamedTemporaryFile('w+t') as version_file: version_file.write('3.0\n') version_file.flush() ar.add(version_file.name, arcname='.relocatable_package_version') with tempfile.TemporaryDirectory() as tmpdir: os.symlink('./pkcs11/p11-kit-trust.so', f'{tmpdir}/libnssckbi.so') ar.reloc_add(f'{tmpdir}/libnssckbi.so', arcname='libreloc/libnssckbi.so') for exe in executables_scylla: basename = os.path.basename(exe) if not args.stripped: fix_executable(ar, exe, f'libexec/{basename}') else: fix_executable(ar, f'{exe}.stripped', f'libexec/{basename}') for exe in executables_distrocmd: basename = os.path.basename(exe) fix_executable(ar, exe, f'libexec/{basename}') for lib, libfile in libs.items(): m = magic.detect_from_filename(libfile) if m and (m.mime_type.startswith('application/x-sharedlib') or m.mime_type.startswith('application/x-pie-executable')): fix_sharedlib(ar, libfile, f'libreloc/{lib}') else: ar.reloc_add(libfile, arcname=lib, recursive=False) if have_gnutls: gnutls_config_nolink = os.path.realpath('/etc/crypto-policies/back-ends/gnutls.config') ar.reloc_add(gnutls_config_nolink, arcname='libreloc/gnutls.config') ar.reloc_add('conf') ar.reloc_add('dist', filter=filter_dist) with tempfile.NamedTemporaryFile('w') as relocatable_file: ar.reloc_add(relocatable_file.name, arcname='SCYLLA-RELOCATABLE-FILE') version_dir = pathlib.Path(args.build_dir) if not (version_dir / 'SCYLLA-RELEASE-FILE').exists(): version_dir = version_dir.parent ar.reloc_add(version_dir / 'SCYLLA-RELEASE-FILE', arcname='SCYLLA-RELEASE-FILE') ar.reloc_add(version_dir / 'SCYLLA-VERSION-FILE', arcname='SCYLLA-VERSION-FILE') ar.reloc_add(version_dir / 'SCYLLA-PRODUCT-FILE', arcname='SCYLLA-PRODUCT-FILE') ar.reloc_add('seastar/scripts') ar.reloc_add('seastar/dpdk/usertools') ar.reloc_add('install.sh') # scylla_post_install.sh lives at the top level together with install.sh in the src tree, but while install.sh is # not distributed in the .rpm and .deb packages, scylla_post_install is, so we'll add it in the package # together with the other scripts that will end up in /usr/lib/scylla ar.reloc_add('scylla_post_install.sh', arcname="dist/common/scripts/scylla_post_install.sh") ar.reloc_add('README.md') ar.reloc_add('NOTICE.txt') ar.reloc_add('ORIGIN') ar.reloc_add('licenses') ar.reloc_add('swagger-ui') ar.reloc_add('api') ar.reloc_add('tools/scyllatop') ar.reloc_add('scylla-gdb.py') ar.reloc_add('bin/nodetool') ar.reloc_add(args.debian_dir, arcname='debian') node_exporter_dir = args.node_exporter_dir if args.stripped: ar.reloc_add(f'{node_exporter_dir}', arcname='node_exporter') ar.reloc_add(f'{node_exporter_dir}/node_exporter.stripped', arcname='node_exporter/node_exporter') else: ar.reloc_add(f'{node_exporter_dir}/node_exporter', arcname='node_exporter/node_exporter') ar.reloc_add(f'{node_exporter_dir}/LICENSE', arcname='node_exporter/LICENSE') ar.reloc_add(f'{node_exporter_dir}/NOTICE', arcname='node_exporter/NOTICE') ar.reloc_add('ubsan-suppressions.supp') ar.reloc_add('fix_system_distributed_tables.py') # Complete the tar output, and wait for the gzip process to complete ar.close() gzip_process.communicate() if gzip_process.returncode != 0: print(f'pigz returned {gzip_process.returncode}!', file=sys.stderr) sys.exit(1)