← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/lp-dev-utils/testr-remote into lp:lp-dev-utils

 

Aaron Bentley has proposed merging lp:~abentley/lp-dev-utils/testr-remote into lp:lp-dev-utils.

Commit message:
Add testr-remote.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/lp-dev-utils/testr-remote/+merge/139249

Add testr-remote to lp-dev-utils
-- 
https://code.launchpad.net/~abentley/lp-dev-utils/testr-remote/+merge/139249
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/lp-dev-utils/testr-remote into lp:lp-dev-utils.
=== modified file '.bzrignore'
--- .bzrignore	2012-08-09 04:39:28 +0000
+++ .bzrignore	2012-12-11 16:13:28 +0000
@@ -8,3 +8,6 @@
 parts
 bin
 develop-eggs
+testr-remote/.testrepository
+testr-remote/servers
+testr-remote/server-locks/*

=== modified file 'cs-test'
--- cs-test	2012-11-09 16:37:50 +0000
+++ cs-test	2012-12-11 16:13:28 +0000
@@ -27,4 +27,9 @@
 su ubuntu -c 'make schema'
 EOT
 . $HOME/.canonistack/novarc
-euca-run-instances ami-00000168 -t m1.small -k ${OS_USERNAME}_$OS_REGION_NAME --user-data-file $cloud_init_script
+if [ -z "$1" ]; then
+  server_count=1;
+else
+  server_count=$1;
+fi
+euca-run-instances -n $server_count ami-00000168 -t m1.small -k ${OS_USERNAME}_$OS_REGION_NAME --user-data-file $cloud_init_script

=== added directory 'testr-remote'
=== added file 'testr-remote/.testr.conf'
--- testr-remote/.testr.conf	1970-01-01 00:00:00 +0000
+++ testr-remote/.testr.conf	2012-12-11 16:13:28 +0000
@@ -0,0 +1,4 @@
+[DEFAULT]
+test_command=./test-remote --subunit $IDOPTION $LISTOPT
+test_id_option=--load-list $IDFILE
+test_list_option=--list-tests

=== added file 'testr-remote/README'
--- testr-remote/README	1970-01-01 00:00:00 +0000
+++ testr-remote/README	2012-12-11 16:13:28 +0000
@@ -0,0 +1,53 @@
+How to use
+==========
+
+You'll need a version of test-repository that supports the --concurrency parameter.  Trunk will do.
+
+To use testr-remote, you first need to initialize the testr-remote directory as a test repository.
+
+To create your instances, use cs-test.
+
+Optionally, use instance-keys to add all the instances' keys to
+.ssh/known-hosts.  This is better than simply accepting the key proposed by
+ssh, because it verifies the key against the fingerprint from the console
+output.
+
+Wait for the instances to finish installing Launchpad.  This usually takes
+about half an hour, so it's good to do this before you need to run any tests.
+You can use a parallel ssh tool (e.g. cluster ssh) to tell you when they're
+done, because the load average will be near zero.
+
+Create a file called 'servers' in the tree root::
+
+  $(./list-servers) > servers
+
+This is just a list of the server names that can be used for ssh.  You can edit
+this list if you like.
+
+Run the tests using "testr-remote".  It's a wrapper around "testr-run" that
+provides the --parallel and --concurrency options, and takes the same
+parameters.  e.g. "testr-remote -- -t test_project" will run
+"bin/test -t test_project" remotely.  (Any parameters that will be passed on to
+bin/test need to be double-escaped.  This is a bug in testr that I have a local
+fix for.)
+
+When you commit new changes, you can use "update-all" to update all servers to
+the newer version.
+
+You can also use switch-all to switch all servers to a new branch.  (This does
+not re-run make).
+
+You can use "euca-terminate-instances $(./list-instance-ids)" to kill all the
+instances when you're done.
+
+How it works
+============
+testr requires a command that it can run to get subunit output, and it needs to
+run it multiple times simultaneously for concurrency.  test-remote is that
+command.  It provides subunit output by ssh-ing into the remote machine and
+running bin/test --subunit.  It uses the server-locks directory to acquire a
+free server.  If none are available, it blocks until one becomes available.
+
+When --parallel is specified, testr supplies a local file containing a list of
+test-ids to use.  test-remote intercepts that option, copies the file to the
+remote server, and supplies the remote path to bin/test.

=== added file 'testr-remote/instance-key'
--- testr-remote/instance-key	1970-01-01 00:00:00 +0000
+++ testr-remote/instance-key	2012-12-11 16:13:28 +0000
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+
+__metaclass__ = type
+
+import os.path
+from optparse import OptionParser
+import re
+from subprocess import Popen, PIPE
+import sys
+from tempfile import NamedTemporaryFile
+import time
+
+
+class Instance:
+
+    def __init__(self, instance_id, ip, servername):
+
+        self.instance_id = instance_id
+        self.ip = ip
+        self.servername = servername
+        self.instance_fingerprint = None
+        self.host_key = None
+
+    def find_fingerprint(self):
+        proc = Popen(['euca-get-console-output', self.instance_id],
+                     stdout=PIPE)
+        lines = iter(proc.stdout)
+        instance_key = None
+        for line in lines:
+            match = re.match('Generating public/private rsa key pair', line)
+            if match is not None:
+                [x for x in zip(lines, range(2))]
+                self.instance_fingerprint = lines.next()[:3*16-1]
+        proc.wait()
+
+
+def iter_instance():
+    proc = Popen(['euca-describe-instances'], stdout=PIPE)
+    try:
+        for num, line in enumerate(proc.stdout):
+            if not line.startswith('INSTANCE'):
+                continue
+            field = line.split('\t')
+            yield Instance(field[1], field[16], field[3])
+    finally:
+        proc.wait()
+
+
+def get_argmap(identifiers):
+    argmap = {}
+    for instance in iter_instance():
+        argmap[instance.instance_id] = instance
+        argmap[instance.ip] = instance
+        argmap[instance.servername] = instance
+    try:
+        return (argmap[identifier] for identifier in identifiers)
+    except KeyError as e:
+        sys.stderr.write('Cannot find %s\n' % e)
+        sys.exit(1)
+
+
+def find_host_keys(instances):
+    ips = [instance.ip for instance in instances]
+    proc = Popen(
+        ['ssh', 'chinstrap.canonical.com', 'ssh-keyscan'] + ips, stdout=PIPE,
+        stderr=PIPE)
+    keys = proc.communicate()[0]
+    key_map = {}
+    for key in keys.splitlines(True):
+        key_map[key.split(' ')[0]] = key
+    for instance in instances:
+        instance.host_key = key_map[instance.ip]
+
+
+def get_key_fingerprint(key):
+    with NamedTemporaryFile() as f:
+        f.write(key)
+        f.flush()
+        proc = Popen(['ssh-keygen', '-lf', f.name], stdout=PIPE)
+        host_fingerprint = proc.communicate()[0]
+        return host_fingerprint.split(' ')[1]
+
+
+def remove_key(known_hosts, hostname):
+    proc = Popen(['ssh-keygen', '-R', hostname, '-f', known_hosts],
+                 stderr=PIPE)
+    proc.communicate()
+
+
+def update_key(instance):
+    known_hosts = os.path.join(os.environ['HOME'], '.ssh/known_hosts')
+    hostnames = [instance.ip, instance.servername,
+                 instance.servername + '.canonistack']
+    for name in hostnames:
+        remove_key(known_hosts, name)
+    with file(known_hosts, 'ab') as f:
+        for name in hostnames:
+            f.write(instance.host_key.replace(instance.ip, name))
+
+def validate_instances(instances):
+    for instance in instances:
+        host_fingerprint = get_key_fingerprint(instance.host_key)
+        yield instance, bool(host_fingerprint == instance.instance_fingerprint)
+
+if __name__ == '__main__':
+    parser = OptionParser()
+    parser.add_option('--poll', action='store_true')
+    options, identifiers = parser.parse_args(sys.argv[1:])
+
+    if len(identifiers) > 0:
+        instances = list(get_argmap(identifiers))
+    else:
+        instances = list(iter_instance())
+
+    for instance in instances:
+        if options.poll:
+            attempt_count = 10
+        else:
+            attempt_count = 1
+        for attempt in range(attempt_count):
+            if attempt != 0:
+                sys.stdout.write('.')
+                sys.stdout.flush()
+                time.sleep(5)
+            instance.find_fingerprint()
+            if instance.instance_fingerprint is not None:
+                break
+        if attempt != 0:
+            sys.stdout.write('\n')
+
+        if instance.instance_fingerprint is None:
+            sys.stderr.write('Cannot find key in console output for %s\n' %
+                             instance.instance_id)
+            sys.exit(1)
+
+    find_host_keys(instances)
+    for instance, valid in validate_instances(instances):
+        if valid:
+            print 'Match.  Adding key for %s.' % instance.ip
+            update_key(instance)
+        else:
+            print "Mismatch.  Not adding key."

=== added file 'testr-remote/list-instance-ids'
--- testr-remote/list-instance-ids	1970-01-01 00:00:00 +0000
+++ testr-remote/list-instance-ids	2012-12-11 16:13:28 +0000
@@ -0,0 +1,2 @@
+#!/bin/sh
+euca-describe-instances | grep '^INSTANCE' | cut -f2

=== added file 'testr-remote/list-servers'
--- testr-remote/list-servers	1970-01-01 00:00:00 +0000
+++ testr-remote/list-servers	2012-12-11 16:13:28 +0000
@@ -0,0 +1,2 @@
+#!/bin/sh
+euca-describe-instances | grep '^INSTANCE' | cut -f4

=== added directory 'testr-remote/server-locks'
=== added file 'testr-remote/switch-all'
--- testr-remote/switch-all	1970-01-01 00:00:00 +0000
+++ testr-remote/switch-all	2012-12-11 16:13:28 +0000
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -e
+push_branch=$(bzr config -d $1 push_location|sed 's/bzr+ssh:\/\/bazaar.launchpad.net\//lp:/')
+revision_id=$(bzr revision-info $1|cut -f2 -d' ')
+echo $push_branch
+echo $revision_id
+servers=$(cat servers)
+for server in $servers; do
+  echo Switching $server ...
+  ssh $server bzr switch -d /opt/launchpad/launchpad $push_branch -r revid:$revision_id
+done

=== added file 'testr-remote/test-remote'
--- testr-remote/test-remote	1970-01-01 00:00:00 +0000
+++ testr-remote/test-remote	2012-12-11 16:13:28 +0000
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+__metaclass__ = type
+
+import argparse
+import errno
+import fcntl
+import os.path
+from contextlib import contextmanager
+import re
+import subprocess
+import sys
+import time
+
+def escape(iput):
+    return re.sub('[^-^a-z^A-Z^_.]', lambda m: '\\'+m.group(0), iput)
+
+
+class ServerSet:
+
+    def __init__(self, servers):
+        self.servers = servers
+
+    @classmethod
+    def from_file(cls):
+        return cls(file('servers').read().splitlines())
+
+    @contextmanager
+    def selected_server(self):
+        while True:
+            for server in self.servers:
+                filename = os.path.join('server-locks', server)
+                with file(filename, 'wb') as lockfile:
+                    try:
+                        fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                    except IOError as e:
+                        if e.errno != errno.EWOULDBLOCK:
+                            raise
+                    else:
+                        try:
+                            yield server
+                        finally:
+                            fcntl.lockf(lockfile, fcntl.LOCK_UN)
+                            return
+            else:
+                time.sleep(1)
+
+
+parser = argparse.ArgumentParser(description='Run some tests remotely.',
+                                 add_help=False)
+parser.add_argument('--load-list', nargs=1, type=argparse.FileType())
+known, unknown = parser.parse_known_args()
+args = [escape(arg) for arg in unknown]
+try:
+    with ServerSet.from_file().selected_server() as server:
+        command = ['ssh', server]
+        if known.load_list is not None:
+            command.extend(['id_list=$(mktemp);',
+        #        'echo $id_list;',
+                'cat > $id_list;'])
+            args = ['--load-list', '$id_list'] + args
+            stdin = known.load_list[0]
+        else:
+            stdin = None
+        command.extend(['cd /opt/launchpad/launchpad;', 'xvfb-run',
+                        'bin/test'])
+        command.extend(args)
+        #print command
+        sys.exit(subprocess.call(command, stdin=stdin))
+except Exception as e:
+    sys.stderr.write(str(e) + '\n')
+    sys.exit(2)

=== added file 'testr-remote/testr-remote'
--- testr-remote/testr-remote	1970-01-01 00:00:00 +0000
+++ testr-remote/testr-remote	2012-12-11 16:13:28 +0000
@@ -0,0 +1,4 @@
+#!/bin/sh
+server_count=$(wc -l servers | cut -f 1 -d ' ')
+concurrency=$((server_count*2))
+~/hacking/testrepository/testr run --parallel --concurrency $concurrency "$@"

=== added file 'testr-remote/update-all'
--- testr-remote/update-all	1970-01-01 00:00:00 +0000
+++ testr-remote/update-all	2012-12-11 16:13:28 +0000
@@ -0,0 +1,2 @@
+#!/bin/sh
+for server in $(cat servers); do ssh $server bzr update /opt/launchpad/launchpad; done