#!/usr/bin/python

import sys
import os
import socket
import logging

from twisted.internet import reactor, error
from twisted.spread import pb
from twisted.cred import credentials

log = logging.getLogger()

class BuildbotClient(object):
    """
    A simple Buildbot client that knows how to connect to a Buildbot master and
    request concurrent builds on multiple builders.

    Sample usage:
      client = BuildbotClient(\"buildmaster\", 9989, \"debugpassword\")
      client.set_builders([\"builder1\", \"builder2\"])
      client.connect()       # connect and start builds running
    """

    # XXX this stops the reactor on failure or success, so it's
    # currently only suitable for use in one-shot command-line scripts

    # Username is always "debug".  Password is configurable in
    # master.cfg, so must be passed to constructor.
    username = "debug"

    def __init__(self, host, port, password):
        self.host = host
        self.port = port
        self.password = password

        self.remote = None              # set by _connect_complete()
        self.num_started = 0            # number of builds started

        # names of builders on which we will start a build (list of str)
        self.builders = None

        # info about user/host requesting the build (logged by the master)
        self.client_script   = os.path.abspath(sys.argv[0])
        self.client_username = os.getenv("USER")
        self.client_hostname = socket.getfqdn()

    def set_builders(self, builders):
        self.builders = builders

    def connect(self):
        """
        Connect to the buildmaster (host and port passed to constructor)
        and start a build on each of the builders passed to set_builders().
        """
        factory = pb.PBClientFactory()
        cred = credentials.UsernamePassword(self.username, self.password)
        d = factory.login(cred)
        reactor.connectTCP(self.host, self.port, factory)
        d.addCallback(self._connect_complete)
        d.addErrback(self._connect_failed)

    def all_started(self):
        return self.num_started == len(self.builders)

    def _connect_complete(self, remote):
        log.info("Connected to buildmaster at %s:%d", self.host, self.port)
        self.remote = remote
        msg = ("client %s successfully connected from %s@%s"
               % (self.client_script, self.client_username, self.client_hostname))
        d = self.remote.callRemote("print", msg)
        d.addCallback(self._start_builds)

    def _connect_failed(self, failure):
        # Blech: for some failures (eg. connection refused), failure.value is
        # meaningful.  But for others (eg. unauthorized login) it is not.
        log.error("error connecting to %s:%d: %s",
                  self.host, self.port, failure.value or failure)
        stopReactor()

    def _start_builds(self, x):
        reason = ("manual build: launched by %s@%s with \"%s\""
                  % (self.client_username,
                     self.client_hostname,
                     " ".join(sys.argv)))
        for builder in self.builders:
            log.debug("Starting a build on builder %r" % builder)

            d = self.remote.callRemote("requestBuild",
                                       builder,
                                       reason,
                                       None,                # branch
                                       None,                # revision
                                      )
            d.addCallback(self._build_started, builder)
            d.addErrback(self._build_start_failed, builder)

    def _build_started(self, x, builder):
        log.info("Builder %r has started a build", builder)
        self.num_started += 1
        if self.all_started():
            stopReactor()

        # For bonus points, we could monitor the builds now and wait until one
        # step has been started in each build.  Later.

    def _build_start_failed(self, failure, builder):
        log.error("Builder %r failed to start a build: %s", builder, failure)
        stopReactor()

def stopReactor():
    try:
        reactor.stop()
    except error.ReactorNotRunning:
        pass

def main():
    if len(sys.argv) != 2:
        sys.exit("usage: %s testnum" % sys.argv[0])
    testnum = int(sys.argv[1])
    builders = ["builder%dA" % testnum, "builder%dB" % testnum]

    # Log to stdout.
    log.addHandler(logging.StreamHandler())
    log.setLevel(logging.INFO)

    # Connect to the slave port (not to a PBListener!).  The password
    # must be the same as configured with 'debugPassword' in master.cfg.
    client = BuildbotClient("localhost", 9989, "debug")
    client.set_builders(builders)

    client.connect()
    reactor.run()

    if client.all_started():
        sys.exit(0)
    else:
        sys.exit(1)

main()
