#! /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 <ip, eg: production db server>
--db_user <user, eg: chromeosqa-admin>
--db_password <password, eg: production 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()