blob: 596b0ebb468f1c0ff3dbad08d13d785be55e6ee7 [file]
#!/usr/bin/env python3
# Copyright Pivotal 2014
import glob
import os
import re
import shutil
import stat
import sys
from optparse import OptionParser
from subprocess import Popen, PIPE, STDOUT
def _getPlatformInfo():
if which('lsb_release') is None:
for file in glob.glob('/etc/*release'):
shutil.copy(file, '.')
else:
Popen('{} -a > ./lsb_release.out'.format(which('lsb_release')),
shell=True, stderr=PIPE)
if os.path.exists('/etc/gpdb-appliance-version'):
shutil.copy('/etc/gpdb-appliance-version', '.')
Popen('uname -r > uname.out', shell=True)
def _getFileInfo(coreFile):
file_cmd = which('file')
if file_cmd is None:
raise Exception("cannot find file command")
cmd = Popen([file_cmd, '--version'], stdout=PIPE, stderr=STDOUT)
fileVersion = cmd.communicate()[0].split()[0].strip().decode()
# file allow setting parameters from command line from version 5.21, refer:
# https://github.com/file/file/commit/6ce24f35cd4a43c4bdd249e8e0c4952c1f8eac67
# Set ELF program sections processed for core files to suppres "too many
# program headers" output
opts = [file_cmd]
if fileVersion >= 'file-5.21':
opts += ['-P', 'elf_phnum=2048']
opts += [coreFile]
cmd = Popen(opts, stdout=PIPE)
return cmd.communicate()[0].decode()
def _isCore(fileCmdOutput):
if fileCmdOutput.find('LSB core file') is -1:
return False
return True
def _findBinary(fileCmdOutput):
# execfn: '/path/to/postgres'
field = str.find(fileCmdOutput, 'execfn')
# if 'execfn' field is not found, search for 'from' field instead
if field < 0:
# from: /path/to/postgres
# from: postgres: 5432, ...
field = str.find(fileCmdOutput, 'from')
# if 'from' field is still missing, search for any single-quoted string
if field < 0:
field = 0
start = str.find(fileCmdOutput, "'", field) + 1
end = str.find(fileCmdOutput, "'", start)
if start <= 0 or end < 0:
return None
cmd = fileCmdOutput[start:end]
# special characters can be correctly handled in abs format, no need to
# remove them
if os.path.isabs(cmd):
return cmd
# otherwise try to search with the process name, punctuations like ':'
# should be removed
cmd = cmd.split()[0].translate(None, str.punctuation)
return which(cmd)
def _getLibraryListWithLDD(binary):
# We manually seed this with a few libraries that are missed
# This may not be needed for all processes, but will round out the
# postgres binary debugging
# TODO: Look at ways to distinguish a 32 vs. 64 bit executable
libraries = [
# on centos
'/lib64/libgcc_s.so.1',
'/lib64/libnss_files.so.2',
'/lib/libgcc_s.so.1',
'/lib/libnss_files.so.2',
# on ubuntu
'/lib/x86_64-linux-gnu/libgcc_s.so.1',
'/lib/x86_64-linux-gnu/libnss_files.so.2',
'/usr/lib32/libgcc_s.so.1',
'/lib32/libnss_files.so.2',
'/usr/libx32/libgcc_s.so.1',
'/libx32/libnss_files.so.2',
]
ldd = which('ldd')
if ldd:
args = [ldd]
else:
# simulate ldd with ld-linux.so
args = ['/lib64/ld-linux-x86-64.so.2', '--list']
ldd_output = Popen(args + [binary], stdout=PIPE)
for line in ldd_output.stdout:
match = re.search(r'(\S+) \(0x', line.decode())
if match and match.group(1):
libraries.append(match.group(1).strip())
return libraries
def _getLibraryListWithGDB(coreFile, binary):
gdb = which('gdb')
if gdb is None:
return False
libraries = []
# fix for issues with PYTHONPATH and PYTHONHOME
environ = os.environ.copy()
for key in ('PYTHONHOME', 'PYTHONPATH', 'LD_LIBRARY_PATH'):
if key in environ:
del environ[key]
cmd = Popen([gdb,
'--batch', # exit after processing options
'--nx', # do not read any .gdbinit files
'--eval-command=info sharedlibrary',
# list shared libraries explicitly
'-c', coreFile,
binary],
stdout=PIPE, stderr=PIPE, env=environ)
result = cmd.communicate()[0].decode()
# gdb output looks like below:
#
# ...
# (gdb) info sharedlibrary
# From To Syms Read Shared Object Library
# 0x00001000 0x00001234 Yes (*) /path/to/liba.so.1.0
# 0x00002000 0x00002234 Yes (*) /path/to/libb.so.1.0
# 0x00003000 0x00003234 Yes /path/to/libc.so.1.0
# (*): Shared library is missing debugging information.
#
# to get the list we first search for the header line, then collect all the
# path strings in following lines.
header = False
for line in result.splitlines():
if header:
begin = line.find(os.path.sep)
if begin >= 0:
libraries.append(line[begin:])
elif 'Shared Object Library' in line:
header = True
return libraries
def _copyFilePath(src, dst):
srcDir = os.path.dirname(src)
if srcDir.find('/') is 0:
srcDir = srcDir[1:]
dstDir = os.path.join(dst, srcDir)
if not os.path.exists(dstDir):
os.makedirs(dstDir)
shutil.copy(src, dstDir)
def _generateGDBScript(b, c):
with open('runGDB.sh', 'w') as f1:
print('''\
#!/bin/bash
unset PYTHONHOME
unset PYTHONPATH
curDIR=`pwd`
/usr/bin/gdb \\
--eval-command="set sysroot $curDIR" \\
--eval-command="core {core}" \\
{binary} \\
"$@"
'''.format(core=c, binary=b), file=f1)
os.chmod('runGDB.sh', 0o0755)
# This is taken from Python 3.3:
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
if path is None:
path = os.environ.get("PATH", os.defpath)
if not path:
return None
path = path.split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try
# others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None
def packCoreFile(coreFile, binary):
packDir = './packcore-' + os.path.basename(coreFile)
packTarball = packDir + '.tgz'
oldDir = os.getcwd()
os.mkdir(packDir)
try:
os.chdir(packDir)
shutil.copy(coreFile, '.')
_getPlatformInfo()
shutil.copy(binary, '.')
libraries = _getLibraryListWithGDB(coreFile, binary)
if libraries is False:
libraries = _getLibraryListWithLDD(binary)
for lib in libraries:
try:
_copyFilePath(lib, '.')
except IOError:
continue
_generateGDBScript(os.path.basename(binary), os.path.basename(coreFile))
os.chdir(oldDir)
cmd = Popen(['tar', 'zcf', packTarball, packDir])
cmd.wait()
except Exception as e:
Popen(['rm', '-rf', packTarball])
raise e
finally:
os.chdir(oldDir)
Popen(['rm', '-rf', packDir])
def parseArgs():
u = '''%prog [options] core_file
This will create an archive with the core file and all required
libraries for analysis. The preference is to use GDB so that we can
resolve dependencies for extensions.'''
parser = OptionParser(version='%prog: $Revision: #1 $', usage=u)
parser.add_option('-b', '--binary', action='store', type='string', dest='binary', metavar='PROGRAMME', help='The full path to the binary that created the core file. Used when packcore cannot determine the source binary')
(option, args) = parser.parse_args()
if len(args) != 1:
parser.error('Please specify a core file')
sys.exit(1)
return (option, args)
def main():
# Check python vesion
if sys.hexversion < 0x020600f0:
sys.stderr.write('packcore requires a minimum python version of 2.6. Current version is:\n' + sys.version)
sys.exit(1)
(options, args) = parseArgs()
coreFile = os.path.abspath(args[0])
fileCmd = _getFileInfo(coreFile)
if not _isCore(fileCmd):
sys.stderr.write(args[0] + ' is not a valid core file\n')
sys.exit(1)
if options.binary:
binary = which(options.binary)
else:
binary = _findBinary(fileCmd)
if not binary:
sys.stderr.write("Unable to find full path to binary for core file\n")
sys.exit(1)
packCoreFile(coreFile, binary)
sys.exit(0)
if __name__ == "__main__":
main()