blob: 57bc50f3ff938f050c28c5f6982f21ef6afe2a3e [file] [log] [blame]
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# This script is to be called by Cloudera Manager to collect Breakpad minidump files up to
# a specified date/time. A compressed tarball is created in the user specified location.
# We try to fit as many files as possible into the tarball until a size limit is reached.
# Example invokation by CM to:
# ./collect_minidumps.py --conf_dir=/var/run/.../5555-impala-STATESTORE/impala-conf \
# --role_name=statestored --max_output_size=50000000 --end_time=1463033495000 \
# --output_file_path=/tmp/minidump_package.tar.gz
from __future__ import absolute_import, division, print_function
import os
import re
import sys
import tarfile
from contextlib import closing
from optparse import OptionParser
class FileArchiver(object):
'''This is a generic class that makes a tarball out of files in the source_dir. We
assume that source_dir contains only files. The resulting file will be compressed with
gzip and placed into output_file_path. If a file with that name already exists, it will
be deleted and re-created. Max_result_size is the maximum allowed size of the resulting
tarball. If all files in the source_dir can't fit into the allowed size, most recent
files will be preferred. start_time and end_time paramenters (in milliseconds UTC) allow
us to specify an interval of time for which to consider the files.
'''
def __init__(self,
source_dir,
output_file_path,
max_output_size,
start_time=None,
end_time=None):
self.source_dir = source_dir
self.max_output_size = max_output_size
self.start_time = start_time
self.end_time = end_time
self.output_file_path = output_file_path
# Maps the number of files in the tarball to the resulting size (in bytes).
self.resulting_sizes = {}
self.file_list = []
def _remove_output_file(self):
try:
os.remove(self.output_file_path)
except OSError:
pass
def _tar_files(self, num_files=None):
'''Make a tarball with num_files most recent files in the file_list. Record the
resulting size into resulting_sizes map and return it.
'''
num_files = num_files or len(self.file_list)
self._remove_output_file()
if num_files == 0:
size = 0
else:
with closing(tarfile.open(self.output_file_path, mode='w:gz')) as out:
for i in range(num_files):
out.add(self.file_list[i])
size = os.stat(self.output_file_path).st_size
self.resulting_sizes[num_files] = size
return size
def _compute_file_list(self):
'''Computes a sorted list of eligible files in the source directory by filtering out
files with modified date not in the desired time range. Directories and other
non-files are ignored.
'''
file_list = []
for f in os.listdir(self.source_dir):
full_path = os.path.join(self.source_dir, f)
if not os.path.isfile(full_path):
continue
# st_mtime is in seconds UTC, so we need to multiply by 1000 to get milliseconds.
time_modified = os.stat(full_path).st_mtime * 1000
if self.start_time and self.start_time > time_modified:
continue
if self.end_time and self.end_time < time_modified:
continue
file_list.append(full_path)
self.file_list = sorted(file_list, key=lambda f: os.stat(f).st_mtime, reverse=True)
def _binary_search(self):
'''Calculates the maximum number of files that can be collected, such that the tarball
size is less than max_output_size.
'''
min_num = 0
max_num = len(self.file_list)
while max_num - min_num > 1:
mid = (min_num + max_num) // 2
if self._tar_files(mid) <= self.max_output_size:
min_num = mid
else:
max_num = mid
return min_num
def make_tarball(self):
'''Make a tarball with the maximum number of files such that the size of the tarball
is less than or equal to max_output_size. Returns a pair (status (int), message
(str)). status represents the result of the operation and follows the unix convention
where 0 equals success. message provides additional information. A status of 1 is
returned if source_dir is not empty and no files were able to fit into the tarball.
'''
self._compute_file_list()
if len(self.file_list) == 0:
status = 0
msg = 'No files found in "{0}".'
return status, msg.format(self.source_dir)
output_size = self._tar_files()
if output_size <= self.max_output_size:
status = 0
msg = 'Success, archived all {0} files in "{1}".'
return status, msg.format(len(self.file_list), self.source_dir)
else:
max_num_files = self._binary_search()
if max_num_files == 0:
self._remove_output_file()
status = 1
msg = ('Unable to archive any files in "{0}". '
'Increase max_output_size to at least {1} bytes.')
# If max_num_files is 0, we are guaranteed that the binary search tried making a
# tarball with 1 file.
return status, msg.format(self.source_dir, self.resulting_sizes[1])
else:
self._tar_files(max_num_files)
status = 0
msg = 'Success. Archived {0} out of {1} files in "{2}".'
return status, msg.format(max_num_files, len(self.file_list), self.source_dir)
def get_config_parameter_value(conf_dir, role_name, config_parameter_name):
'''Extract a single config parameter from the configuration file of a particular
daemon.
'''
ROLE_FLAGFILE_MAP = {
'impalad': 'impalad_flags',
'statestored': 'state_store_flags',
'catalogd': 'catalogserver_flags'}
config_parameter_value = None
try:
file_path = os.path.join(conf_dir, ROLE_FLAGFILE_MAP[role_name])
with open(file_path, 'r') as f:
for line in f:
m = re.match('-{0}=(.*)'.format(config_parameter_name), line)
if m:
config_parameter_value = m.group(1)
except IOError as e:
print('Error: Unable to open "{0}".'.format(file_path), file=sys.stderr)
sys.exit(1)
return config_parameter_value
def get_minidump_dir(conf_dir, role_name):
'''Extracts the minidump directory path for a given role from the configuration file.
The directory defaults to 'minidumps', relative paths are prepended with log_dir, which
defaults to '/tmp'.
'''
minidump_path = get_config_parameter_value(
conf_dir, role_name, 'minidump_path') or 'minidumps'
if not os.path.isabs(minidump_path):
log_dir = get_config_parameter_value(conf_dir, role_name, 'log_dir') or '/tmp'
minidump_path = os.path.join(log_dir, minidump_path)
result = os.path.join(minidump_path, role_name)
if not os.path.isdir(result):
msg = 'Error: minidump directory does not exist.'
print(msg, file=sys.stderr)
sys.exit(1)
return result
def main():
parser = OptionParser()
parser.add_option('--conf_dir',
help='Directory in which to look for the config file with startup flags')
parser.add_option('--role_name', type='choice',
choices=['impalad', 'statestored', 'catalogd'], default='impalad',
help='For which role to collect the minidumps.')
parser.add_option('--max_output_size', default=40*1024*1024, type='int',
help='The maximum file size of the result tarball to be written given in bytes. '
'If the total size exceeds this value, most recent files will be preferred')
parser.add_option('--start_time', default=None, type='int',
help='Interval start time (in epoch milliseconds UTC).')
parser.add_option('--end_time', default=None, type='int',
help='Interval end time, until when to collect the minidump files '
'(in epoch milliseconds UTC).')
parser.add_option('--output_file_path', help='The full path of the output file.')
options, args = parser.parse_args()
if not options.conf_dir:
msg = 'Error: conf_dir is not specified.'
print(msg, file=sys.stderr)
sys.exit(1)
if not options.output_file_path:
msg = 'Error: output_file_path is not specified.'
print(msg, file=sys.stderr)
sys.exit(1)
minidump_dir = get_minidump_dir(options.conf_dir, options.role_name)
file_archiver = FileArchiver(source_dir=minidump_dir,
max_output_size=options.max_output_size,
start_time=options.start_time,
end_time=options.end_time,
output_file_path=options.output_file_path)
status, msg = file_archiver.make_tarball()
print(msg, file=sys.stderr)
sys.exit(status)
if __name__ == '__main__':
main()