普通文本  |  196行  |  6.46 KB

#! /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()