#!/usr/bin/python3 -u # Copyright 2021 Memgraph Ltd. # # Use of this software is governed by the Business Source License # included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source # License, and you may not use this file except in compliance with the Business Source License. # # As of the Change Date specified in that file, in accordance with # the Business Source License, use of this software will be governed # by the Apache License, Version 2.0, included in the file # licenses/APL.txt. import argparse import itertools import json import os import signal import sys import time from http.server import BaseHTTPRequestHandler, HTTPServer def build_handler(storage, args): class Handler(BaseHTTPRequestHandler): def do_HEAD(self): assert False def do_GET(self): assert False def do_PUT(self): assert False def do_POST(self): if args.redirect and self.path == args.path: # 307 is used instead of 301 to preserve body data # https://stackoverflow.com/questions/19070801/curl-loses-body-when-a-post-redirected-from-http-to-https self.send_response(307) self.send_header("Location", args.redirect_path) self.end_headers() return assert self.headers["user-agent"] == "memgraph/telemetry" assert self.headers["accept"] == "application/json" assert self.headers["content-type"] == "application/json" content_len = int(self.headers.get("content-length", 0)) data = json.loads(self.rfile.read(content_len).decode("utf-8")) if self.path not in [args.path, args.redirect_path]: self.send_response(404) self.end_headers() return if args.no_response_count > 0: args.no_response_count -= 1 return if args.wrong_code_count > 0: args.wrong_code_count -= 1 self.send_response(500) self.end_headers() return assert type(data) == list for item in data: assert type(item) == dict assert "event" in item assert "run_id" in item assert "type" in item assert "machine_id" in item assert "data" in item assert "timestamp" in item storage.append(item) if args.hang: time.sleep(20) self.send_response(200) self.end_headers() return Handler class Server(HTTPServer): def handle_error(self, request, client_address): super().handle_error(request, client_address) os._exit(1) def shutdown(self): # TODO: this is a hack. The parent object implementation of this # function sets the shutdown flag and then waits for the shutdown to # complete. We only need to set the shutdown flag because we don't # want to run the server in another thread. The parent implementation # can be seen here: # https://github.com/python/cpython/blob/3.5/Lib/socketserver.py#L241 self._BaseServer__shutdown_request = True def item_sort_key(obj): if type(obj) != dict: return -1 if "timestamp" not in obj: return -1 return obj["timestamp"] def verify_storage(storage, args): rid = storage[0]["run_id"] timestamp = 0 for i, item in enumerate(storage): assert item["run_id"] == rid assert item["timestamp"] >= timestamp timestamp = item["timestamp"] if i == 0: assert item["event"] == "startup" elif i == len(storage) - 1: assert item["event"] == "shutdown" else: assert item["event"] == i - 1 if i == 0: assert "architecture" in item["data"] assert "cpu_count" in item["data"] assert "cpu_model" in item["data"] assert "kernel" in item["data"] assert "memory" in item["data"] assert "os" in item["data"] assert "swap" in item["data"] else: assert item["data"]["db"]["vertices"] == i assert item["data"]["db"]["edges"] == i assert "resources" in item["data"] assert "cpu" in item["data"]["resources"] assert "memory" in item["data"]["resources"] assert "uptime" in item["data"] uptime = item["data"]["uptime"] expected = i * args.interval if i == len(storage) - 1: if not args.no_check_duration: expected = args.duration else: expected = uptime assert uptime >= expected - 4 and uptime <= expected + 4 if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--address", type=str, default="127.0.0.1") parser.add_argument("--port", type=int, default=9000) parser.add_argument("--path", type=str, default="/") parser.add_argument("--redirect", action="store_true") parser.add_argument("--no-response-count", type=int, default=0) parser.add_argument("--wrong-code-count", type=int, default=0) parser.add_argument("--no-check", action="store_true") parser.add_argument("--hang", action="store_true") parser.add_argument("--interval", type=int, default=1) parser.add_argument("--duration", type=int, default=10) parser.add_argument("--startups", type=int, default=1) parser.add_argument("--no-check-duration", action="store_true") args = parser.parse_args() args.redirect_path = os.path.join(args.path, "redirect") storage = [] handler = build_handler(storage, args) httpd = Server((args.address, args.port), handler) signal.signal(signal.SIGTERM, lambda signum, frame: httpd.shutdown()) httpd.serve_forever() httpd.server_close() if args.no_check: sys.exit(0) # Order the received data. storage.sort(key=item_sort_key) # Split the data into individual startups. startups = [[storage[0]]] for item in storage[1:]: if item["run_id"] != startups[-1][-1]["run_id"]: startups.append([]) startups[-1].append(item) # Check that there were the correct number of startups. assert len(startups) == args.startups, f"Expected: {args.startups}, actual: {len(startups)}" # Verify each startup. for startup in startups: verify_storage(startup, args) # machine id has to be same for every run on the same machine assert len(set(map(lambda x: x["machine_id"], itertools.chain(*startups)))) == 1