#!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed 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. import argparse import gzip import io import os import re import sys import urllib.request as urlreq import yaml # DEVSTACK_GATE_GRENADE is either unset if grenade is not running # or a string describing what type of grenade run to perform. is_grenade = os.environ.get('DEVSTACK_GATE_GRENADE') is not None dump_all_errors = True # As logs are made clean, remove from this set allowed_dirty = set([ 'c-api', 'ceilometer-acentral', 'ceilometer-acompute', 'ceilometer-alarm-evaluator', 'ceilometer-anotification', 'ceilometer-api', 'ceilometer-collector', 'c-vol', 'g-api', 'h-api', 'h-eng', 'ir-cond', 'n-api', 'n-cpu', 'n-net', 'q-agt', 'q-dhcp', 'q-lbaas', 'q-meta', 'q-metering', 'q-svc', 's-proxy']) def process_files(file_specs, url_specs, allow_lists): regexp = re.compile(r"^.* (ERROR|CRITICAL|TRACE) .*\[.*\-.*\]") logs_with_errors = [] for (name, filename) in file_specs: allow_list = allow_lists.get(name, []) with open(filename) as content: if scan_content(content, regexp, allow_list): logs_with_errors.append(name) for (name, url) in url_specs: allow_list = allow_lists.get(name, []) req = urlreq.Request(url) req.add_header('Accept-Encoding', 'gzip') page = urlreq.urlopen(req) buf = io.StringIO(page.read()) f = gzip.GzipFile(fileobj=buf) if scan_content(f.read().splitlines(), regexp, allow_list): logs_with_errors.append(name) return logs_with_errors def scan_content(content, regexp, allow_list): had_errors = False for line in content: if not line.startswith("Stderr:") and regexp.match(line): allowed = False for w in allow_list: pat = ".*%s.*%s.*" % (w['module'].replace('.', '\\.'), w['message']) if re.match(pat, line): allowed = True break if not allowed or dump_all_errors: if not allowed: had_errors = True return had_errors def collect_url_logs(url): page = urlreq.urlopen(url) content = page.read() logs = re.findall(r'(screen-[\w-]+\.txt\.gz)', content) return logs def main(opts): if opts.directory and opts.url or not (opts.directory or opts.url): print("Must provide exactly one of -d or -u") return 1 print("Checking logs...") ALLOW_LIST_FILE = os.path.join( os.path.abspath(os.path.dirname(os.path.dirname(__file__))), "etc", "allow-list.yaml") file_matcher = re.compile(r".*screen-([\w-]+)\.log") files = [] if opts.directory: d = opts.directory for f in os.listdir(d): files.append(os.path.join(d, f)) files_to_process = [] for f in files: m = file_matcher.match(f) if m: files_to_process.append((m.group(1), f)) url_matcher = re.compile(r".*screen-([\w-]+)\.txt\.gz") urls = [] if opts.url: for logfile in collect_url_logs(opts.url): urls.append("%s/%s" % (opts.url, logfile)) urls_to_process = [] for u in urls: m = url_matcher.match(u) if m: urls_to_process.append((m.group(1), u)) allow_lists = {} with open(ALLOW_LIST_FILE) as stream: loaded = yaml.safe_load(stream) if loaded: for (name, l) in loaded.items(): for w in l: assert 'module' in w, 'no module in %s' % name assert 'message' in w, 'no message in %s' % name allow_lists = loaded logs_with_errors = process_files(files_to_process, urls_to_process, allow_lists) failed = False if logs_with_errors: log_files = set(logs_with_errors) for log in log_files: msg = '%s log file has errors' % log if log not in allowed_dirty: msg += ' and is not allowed to have them' failed = True print(msg) print("\nPlease check the respective log files to see the errors") if failed: if is_grenade: print("Currently not failing grenade runs with errors") return 0 return 1 print("ok") return 0 usage = """ Find non-allow-listed log errors in log files from a devstack-gate run. Log files will be searched for ERROR or CRITICAL messages. If any error messages do not match any of the allow-list entries contained in etc/allow-list.yaml, those messages will be printed to the console and failure will be returned. A file directory containing logs or a url to the log files of an OpenStack gate job can be provided. The allow-list yaml looks like: log-name: - module: "a.b.c" message: "regexp" - module: "a.b.c" message: "regexp" repeated for each log file with an allow-list. """ parser = argparse.ArgumentParser(description=usage) parser.add_argument('-d', '--directory', help="Directory containing log files") parser.add_argument('-u', '--url', help="url containing logs from an OpenStack gate job") if __name__ == "__main__": try: sys.exit(main(parser.parse_args())) except Exception as e: print("Failure in script: %s" % e) # Don't fail if there is a problem with the script. sys.exit(0)