blob: d9f218e551cf914d5330f586749b304f8c89e93f [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.
#
##
# Jenkins/Travis tools script, for monitoring and analyzing jobs from CI.
#
# Example use to monitor a travis build, job number N:
# > citool monitor N
#
# To monitor same job until completion:
# > citool monitor -p N
#
# To save job output to a file:
# > citool -o monitor N
#
# Example use to monitor a jenkins build B with job number N:
# > citool -u https://jenkins.host:port -b B monitor N
##
import sys
import time
import traceback
import argparse
if sys.version_info.major >= 3:
from http.client import HTTPConnection, HTTPSConnection, TEMPORARY_REDIRECT, OK
from urllib.parse import urlparse
else:
from httplib import HTTPConnection, HTTPSConnection, TEMPORARY_REDIRECT, OK
from urlparse import urlparse
import re
import threading
import json
from xml.dom import minidom
from subprocess import Popen, PIPE, STDOUT
def main():
exitCode = 0
try:
args = parseArgs()
exitCode = {
'monitor' : monitor,
'cat': cat
}[args.cmd](args)
except Exception as e:
print('Exception: ', e)
if args.verbose:
traceback.print_exc()
exitCode = 1
sys.exit(exitCode)
def parseArgs():
parser = argparse.ArgumentParser(description='tool for analyzing logs from CI')
subparsers = parser.add_subparsers(title='available commands', dest='cmd')
parser.add_argument('job', help='job number; for matrix jobs add the matrix index after a period (e.g., 401881768.2)')
parser.add_argument('-b', '--build', help='build name', default='travis')
parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')
parser.add_argument('-i', '--input-file', help='read logs from file rather than CI', action='store_true', dest='ifile')
parser.add_argument('-o', '--output-file', help='store intermediate buffer to a file (e.g., jenkins console or component logs)', action='store_true', dest='ofile')
parser.add_argument('-u', '--url', help='URL for CI build job (default is Travis CI)', default='https://api.travis-ci.org')
subparser = subparsers.add_parser('monitor', help='report passing or failing tests (only failing tests by default)')
subparser.add_argument('-a', '--all', help='show all tests suites, passing and failing', action='store_true')
subparser.add_argument('-p', '--poll', help='repeat monitor every 10 seconds', action='store_true')
subparser = subparsers.add_parser('cat', help='concatenate logs from build (limited to Jenkins)')
subparser.add_argument('artifactPath', help='path to artifacts store')
subparser.add_argument('-g', '--grep', help='run grep against logs using provided value')
subparser.add_argument('-s', '--sort', help='sort logs by timestamp', action='store_true')
subparser.add_argument('-n', '--invokers', help='number of invokers', type=int, default=3)
subparser.add_argument('-c', '--controllers', help='number of controllers', type=int, default=1)
return parser.parse_args()
def request(method, urlString, body = "", headers = {}, auth = None, verbose = False):
url = urlparse(urlString)
if url.scheme == 'http':
conn = HTTPConnection(url.netloc)
else:
conn = HTTPSConnection(url.netloc)
if verbose:
print("%s %s" % (method, urlString))
conn.request(method.upper(), urlString, body, headers = headers)
res = conn.getresponse()
if verbose:
print('Got response with code %s' % res.status)
return res
def shell(cmd, data = None, verbose = False):
start = time.time()
if verbose:
print('%s%s' % (cmd, ' <stdin>' if (data) else ''))
p = Popen(cmd, shell = True, stdout = PIPE, stderr = STDOUT, stdin = PIPE)
out, err = p.communicate(input = data)
p.wait()
# stdout/stderr may be either text or bytes, depending on Python
# version. In the latter case, decode to text.
if isinstance(out, bytes):
out = out.decode('utf-8')
if isinstance(err, bytes):
err = err.decode('utf-8')
end = time.time()
delta = end - start
return (delta, out, err)
def getTravisHeaders():
return {'User-Agent': 'wsk citool/0.0.1',
'Travis-API-Version': 3,
'Accept': 'application/vnd.travis-ci.2+json'
}
def getTravisMatrixId(parts, values):
N = len(values)
if len(parts) == 1:
return -1
else:
try:
matrix = int(parts[1]) -1
if matrix < 0:
print('Matrix id must be positive. Valid values are [1..%s].' % N)
exit(-1)
if matrix >= N:
print('Matrix id is out of bounds. Valid values are [1..%s].' % N)
exit(-1)
return matrix
except Exception:
print('Matrix id is not an integer as expected. Valid values are [1..%s].' % N)
exit(-1)
def getJobUrl(args):
if args.build.lower() == 'travis':
# Get build information
parts = args.job.split('.')
jobid = parts[0]
if len(parts) > 2:
print('Job is malformed')
exit(-1)
buildUrl = '%s/build/%s' % (args.url, jobid)
buildRes = request('get', buildUrl, headers = getTravisHeaders(), verbose = args.verbose)
body = validateResponse(buildRes)
try:
body = json.loads(body)
index = getTravisMatrixId(parts, body['jobs'])
job = body['jobs'][index]['id']
except Exception:
print('Expected response to contain build and job-ids properties in %s' % body)
exit(-1)
url = '%s/job/%s' % (args.url, job)
else: # assume jenkins
url = '%s/job/%s/%s' % (args.url, args.build, args.job)
return url
def monitor(args):
def poll():
(ex, finished) = monitorOnce(args)
if not finished:
threading.Timer(10.0, poll).start()
if args.poll:
poll()
else:
(ex, finished) = monitorOnce(args)
return ex
def monitorOnce(args):
if args.ifile:
file = open('%s' % args.job, 'r')
body = file.read()
file.close()
else:
if args.build.lower() == 'travis':
url = '%s/log.txt' % getJobUrl(args)
res = request('get', url, headers = getTravisHeaders(), verbose = args.verbose)
if res.status == TEMPORARY_REDIRECT:
url = res.getheader('location')
res = request('get', url, headers = getTravisHeaders(), verbose = args.verbose)
else: # assume jenkins
url = '%s/logText/progressiveHtml' % getJobUrl(args)
res = request('get', url, verbose = args.verbose)
body = validateResponse(res)
if args.ofile:
file = open('%s-console.log' % args.job, 'w')
file.write(body)
file.close()
if args.ifile or res.status == OK:
grepForFailingTests(args, body)
return reportBuildStatus(args, body)
elif args.ofile is False:
print(body)
return res.status
def validateResponse(res):
body = res.read()
if res.status != OK:
if body.startswith('<'):
dom = minidom.parseString(body)
print(dom.toprettyxml()),
else:
print(body)
exit(res.status)
elif not body:
print('Build log is empty.')
exit(-1)
else:
return body
def grepForFailingTests(args, body):
cmd = 'grep :tests:test'
# check that tests ran
(time, output, error) = shell(cmd, body, args.verbose)
if output == '':
print('No tests detected.')
# no tests: either build failure or task not yet reached, skip further check
else:
cmd = 'grep -E "^\w+\.*.*[&gt;|>] \w*.* FAILED%s"' % ("|PASSED" if args.all else "")
(time, output, error) = shell(cmd, body, args.verbose)
if output == '':
print('All tests passing.')
else:
print(output.replace('&gt;', '>')),
def reportBuildStatus(args, body):
lines = body.decode('utf8').rstrip('\n').rsplit('\n', 1)
if len(lines) == 2:
output = lines[1]
output = re.sub('<[^<]+?>', '', output).strip()
else:
output = None
if output and ('Finished: ' in output or output.startswith('Done.') or ('exceeded' in output and 'terminated' in output)):
print(output)
return (0, True)
else:
print('Build: ONGOING')
if output:
print(output)
return (0, False)
def cat(args):
def getComponentList(components):
list = []
for k,v in components.items():
if v > 1:
for i in range(v):
list.append('%s%d' % (k, i))
else:
list.append(k)
return list
def getComponentLogs(component):
url = '%s/artifact/%s/%s/%s_logs.log' % (getJobUrl(args), args.artifactPath, component, component)
res = request('get', url, verbose = args.verbose)
body = res.read()
if res.status == OK:
return body
else:
return ''
def unzip(iterable):
return zip(*iterable)
def extractDate(line):
matches = re.search(r'\d{4}-[01]{1}\d{1}-[0-3]{1}\d{1}T[0-2]{1}\d{1}:[0-6]{1}\d{1}:[0-6]{1}\d{1}.\d{3}Z', line)
if matches is not None:
date = matches.group(0)
return date
else:
return None
if args.ifile:
file = open('%s-build.log' % args.job, 'r')
joined = file.read()
file.close()
elif args.build.lower == 'travis':
print('Feature not yet supported for Travis builds.')
return 2
else:
components = {
'controller': args.controllers,
'invoker': args.invokers
}
logs = map(getComponentLogs, getComponentList(components))
joined = ''.join(logs)
if args.ofile:
file = open('%s-build.log' % args.job, 'w')
file.write(joined)
file.close()
if args.grep is not None:
cmd = 'grep "%s"' % args.grep
(time, output, error) = shell(cmd, joined, args.verbose)
output = output.strip()
if args.sort:
parts = output.split('\n')
filter = [p for p in parts if p != '']
date = map(extractDate, filter)
keyed = zip(date, parts)
sort = sorted(keyed, key=lambda t: t[1])
msgs = unzip(sort)[1]
print('\n'.join(msgs))
return 0
else:
print(output)
return 0
elif args.ofile is False:
print(joined)
return 0
if __name__ == '__main__':
main()