#! /usr/bin/python """A simple heartbeat server. Executes *readonly* heartbeats against the given database. Usage: 1. heartbeat_server.py --port 8080 Start to serve heartbeats on port 8080 using the database credentials found in the shadow_config. One would perform heartbeats for board:lumpy against this server with: curl http://localhost:8080/lumpy. Or just visiting the url through the browser. Such a server is capable of handling the following urls: /lumpy: Return formatted heartbeat packets with timing information for each stage, to be viewed in the browser. /lumpy?raw: Return raw json heartbeat packets for lumpy /lumpy?raw&host_limit=1&job_limit=0: Return a 'raw' heartbeat with the first host and not jobs. 2. heartbeat_server.py --db_host --db_user --db_password The same as 1. but use the remote db server specified via db_(host,user,password). """ import argparse import sys import time import urlparse from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer import common from autotest_lib.client.common_lib.global_config import global_config as config from autotest_lib.frontend import setup_django_environment # Populated with command line database credentials. DB_SETTINGS = { 'ENGINE': 'autotest_lib.frontend.db.backends.afe', } # Indent level used when formatting json for the browser. JSON_FORMATTING_INDENT = 4 def time_call(func): """A simple timer wrapper. @param func: The function to wrap. """ def wrapper(*args, **kwargs): """Wrapper returned by time_call decorator.""" start = time.time() res = func(*args, **kwargs) return time.time()-start, res return wrapper class BoardHandler(BaseHTTPRequestHandler): """Handles heartbeat urls.""" # Prefix for all board labels. board_prefix = 'board:' @staticmethod @time_call def _get_jobs(board, job_limit=None): jobs = models.Job.objects.filter( dependency_labels__name=board).exclude( hostqueueentry__complete=True).exclude( hostqueueentry__active=True) return jobs[:job_limit] if job_limit is not None else jobs @staticmethod @time_call def _get_hosts(board, host_limit=None): hosts = models.Host.objects.filter( labels__name__in=[board], leased=False) return hosts[:host_limit] if host_limit is not None else hosts @staticmethod @time_call def _create_packet(hosts, jobs): return { 'hosts': [h.serialize() for h in hosts], 'jobs': [j.serialize() for j in jobs] } def do_GET(self): """GET handler. Handles urls like: http://localhost:8080/lumpy?raw&host_limit=5 and writes the appropriate http response containing the heartbeat. """ parsed_path = urlparse.urlparse(self.path, allow_fragments=True) board = '%s%s' % (self.board_prefix, parsed_path.path.rsplit('/')[-1]) raw = False job_limit = None host_limit = None for query in parsed_path.query.split('&'): split_query = query.split('=') if split_query[0] == 'job_limit': job_limit = int(split_query[1]) elif split_query[0] == 'host_limit': host_limit = int(split_query[1]) elif split_query[0] == 'raw': raw = True host_time, hosts = self._get_hosts(board, host_limit) job_time, jobs = self._get_jobs(board, job_limit) serialize_time, heartbeat_packet = self._create_packet(hosts, jobs) self.send_response(200) self.end_headers() # Format browser requests, the heartbeat client will request using ?raw # while the browser will perform a plain request like # http://localhost:8080/lumpy. The latter needs to be human readable and # include more details timing information. json_encoder = django_encoder.DjangoJSONEncoder() if not raw: json_encoder.indent = JSON_FORMATTING_INDENT self.wfile.write('Serialize: %s,\nJob query: %s\nHost query: %s\n' 'Hosts: %s\nJobs: %s\n' % (serialize_time, job_time, host_time, len(heartbeat_packet['hosts']), len(heartbeat_packet['jobs']))) self.wfile.write(json_encoder.encode(heartbeat_packet)) return def _parse_args(args): parser = argparse.ArgumentParser( description='Start up a simple heartbeat server on localhost.') parser.add_argument( '--port', default=8080, help='The port to start the heartbeat server.') parser.add_argument( '--db_host', default=config.get_config_value('AUTOTEST_WEB', 'host'), help='Db server ip address.') parser.add_argument( '--db_name', default=config.get_config_value('AUTOTEST_WEB', 'database'), help='Name of the db table.') parser.add_argument( '--db_user', default=config.get_config_value('AUTOTEST_WEB', 'user'), help='User for the db server.') parser.add_argument( '--db_password', default=config.get_config_value('AUTOTEST_WEB', 'password'), help='Password for the db server.') parser.add_argument( '--db_port', default=config.get_config_value('AUTOTEST_WEB', 'port', default=''), help='Port of the db server.') return parser.parse_args(args) if __name__ == '__main__': args = _parse_args(sys.argv[1:]) server = HTTPServer(('localhost', args.port), BoardHandler) print ('Starting heartbeat server, query eg: http://localhost:%s/lumpy' % args.port) # We need these lazy imports to allow command line specification of # database credentials. from autotest_lib.frontend import settings DB_SETTINGS['HOST'] = args.db_host DB_SETTINGS['NAME'] = args.db_name DB_SETTINGS['USER'] = args.db_user DB_SETTINGS['PASSWORD'] = args.db_password DB_SETTINGS['PORT'] = args.db_port settings.DATABASES['default'] = DB_SETTINGS from autotest_lib.frontend.afe import models from django.core.serializers import json as django_encoder server.serve_forever()