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

Proposed by Aaron Bentley
Status: Rejected
Rejected by: William Grant
Proposed branch: lp:~abentley/lp-dev-utils/testr-remote
Merge into: lp:lp-dev-utils
Diff against target: 363 lines (+300/-1)
11 files modified
.bzrignore (+3/-0)
cs-test (+6/-1)
testr-remote/.testr.conf (+4/-0)
testr-remote/README (+53/-0)
testr-remote/instance-key (+142/-0)
testr-remote/list-instance-ids (+2/-0)
testr-remote/list-servers (+2/-0)
testr-remote/switch-all (+11/-0)
testr-remote/test-remote (+71/-0)
testr-remote/testr-remote (+4/-0)
testr-remote/update-all (+2/-0)
To merge this branch: bzr merge lp:~abentley/lp-dev-utils/testr-remote
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+139249@code.launchpad.net

Commit message

Add testr-remote.

Description of the change

Add testr-remote to lp-dev-utils

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. Thanks for working on this. I have approved
the branch but I think you will want to address a couple of the things
below before landing it.

Do you really want the $() wrapping ./list-servers on line 62 of the
diff?

I do not understand the third and fourth caret characters in the regex
on line 291 of the diff). As far as I can tell they are unnecessary.

The commented-out code on line 334 and 343 of the diff should be
removed.

I doubt you intend to prescribe the location of the testr binary so
precisely on line 356 of the diff.

The exit(2) on line 347 of the diff has prompted me to climb the nearest
snow-covered peak and meditate for three days. I have yet to discover
the deep motivations that resulted in exit(2) instead of exit(1), but I
thank you for the opportunity to start on the path toward enlightenment.
Next I will try walking barefoot across a desert, that or you could add
a comment.

review: Approve (code)

Unmerged revisions

118. By Aaron Bentley

Doc tweaks.

117. By Aaron Bentley

Migrate ignore rules.

116. By Aaron Bentley

Added testr-remote.

115. By Aaron Bentley

Allow specifying the number of instances to run.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2012-08-09 04:39:28 +0000
3+++ .bzrignore 2012-12-11 16:13:28 +0000
4@@ -8,3 +8,6 @@
5 parts
6 bin
7 develop-eggs
8+testr-remote/.testrepository
9+testr-remote/servers
10+testr-remote/server-locks/*
11
12=== modified file 'cs-test'
13--- cs-test 2012-11-09 16:37:50 +0000
14+++ cs-test 2012-12-11 16:13:28 +0000
15@@ -27,4 +27,9 @@
16 su ubuntu -c 'make schema'
17 EOT
18 . $HOME/.canonistack/novarc
19-euca-run-instances ami-00000168 -t m1.small -k ${OS_USERNAME}_$OS_REGION_NAME --user-data-file $cloud_init_script
20+if [ -z "$1" ]; then
21+ server_count=1;
22+else
23+ server_count=$1;
24+fi
25+euca-run-instances -n $server_count ami-00000168 -t m1.small -k ${OS_USERNAME}_$OS_REGION_NAME --user-data-file $cloud_init_script
26
27=== added directory 'testr-remote'
28=== added file 'testr-remote/.testr.conf'
29--- testr-remote/.testr.conf 1970-01-01 00:00:00 +0000
30+++ testr-remote/.testr.conf 2012-12-11 16:13:28 +0000
31@@ -0,0 +1,4 @@
32+[DEFAULT]
33+test_command=./test-remote --subunit $IDOPTION $LISTOPT
34+test_id_option=--load-list $IDFILE
35+test_list_option=--list-tests
36
37=== added file 'testr-remote/README'
38--- testr-remote/README 1970-01-01 00:00:00 +0000
39+++ testr-remote/README 2012-12-11 16:13:28 +0000
40@@ -0,0 +1,53 @@
41+How to use
42+==========
43+
44+You'll need a version of test-repository that supports the --concurrency parameter. Trunk will do.
45+
46+To use testr-remote, you first need to initialize the testr-remote directory as a test repository.
47+
48+To create your instances, use cs-test.
49+
50+Optionally, use instance-keys to add all the instances' keys to
51+.ssh/known-hosts. This is better than simply accepting the key proposed by
52+ssh, because it verifies the key against the fingerprint from the console
53+output.
54+
55+Wait for the instances to finish installing Launchpad. This usually takes
56+about half an hour, so it's good to do this before you need to run any tests.
57+You can use a parallel ssh tool (e.g. cluster ssh) to tell you when they're
58+done, because the load average will be near zero.
59+
60+Create a file called 'servers' in the tree root::
61+
62+ $(./list-servers) > servers
63+
64+This is just a list of the server names that can be used for ssh. You can edit
65+this list if you like.
66+
67+Run the tests using "testr-remote". It's a wrapper around "testr-run" that
68+provides the --parallel and --concurrency options, and takes the same
69+parameters. e.g. "testr-remote -- -t test_project" will run
70+"bin/test -t test_project" remotely. (Any parameters that will be passed on to
71+bin/test need to be double-escaped. This is a bug in testr that I have a local
72+fix for.)
73+
74+When you commit new changes, you can use "update-all" to update all servers to
75+the newer version.
76+
77+You can also use switch-all to switch all servers to a new branch. (This does
78+not re-run make).
79+
80+You can use "euca-terminate-instances $(./list-instance-ids)" to kill all the
81+instances when you're done.
82+
83+How it works
84+============
85+testr requires a command that it can run to get subunit output, and it needs to
86+run it multiple times simultaneously for concurrency. test-remote is that
87+command. It provides subunit output by ssh-ing into the remote machine and
88+running bin/test --subunit. It uses the server-locks directory to acquire a
89+free server. If none are available, it blocks until one becomes available.
90+
91+When --parallel is specified, testr supplies a local file containing a list of
92+test-ids to use. test-remote intercepts that option, copies the file to the
93+remote server, and supplies the remote path to bin/test.
94
95=== added file 'testr-remote/instance-key'
96--- testr-remote/instance-key 1970-01-01 00:00:00 +0000
97+++ testr-remote/instance-key 2012-12-11 16:13:28 +0000
98@@ -0,0 +1,142 @@
99+#!/usr/bin/env python
100+
101+__metaclass__ = type
102+
103+import os.path
104+from optparse import OptionParser
105+import re
106+from subprocess import Popen, PIPE
107+import sys
108+from tempfile import NamedTemporaryFile
109+import time
110+
111+
112+class Instance:
113+
114+ def __init__(self, instance_id, ip, servername):
115+
116+ self.instance_id = instance_id
117+ self.ip = ip
118+ self.servername = servername
119+ self.instance_fingerprint = None
120+ self.host_key = None
121+
122+ def find_fingerprint(self):
123+ proc = Popen(['euca-get-console-output', self.instance_id],
124+ stdout=PIPE)
125+ lines = iter(proc.stdout)
126+ instance_key = None
127+ for line in lines:
128+ match = re.match('Generating public/private rsa key pair', line)
129+ if match is not None:
130+ [x for x in zip(lines, range(2))]
131+ self.instance_fingerprint = lines.next()[:3*16-1]
132+ proc.wait()
133+
134+
135+def iter_instance():
136+ proc = Popen(['euca-describe-instances'], stdout=PIPE)
137+ try:
138+ for num, line in enumerate(proc.stdout):
139+ if not line.startswith('INSTANCE'):
140+ continue
141+ field = line.split('\t')
142+ yield Instance(field[1], field[16], field[3])
143+ finally:
144+ proc.wait()
145+
146+
147+def get_argmap(identifiers):
148+ argmap = {}
149+ for instance in iter_instance():
150+ argmap[instance.instance_id] = instance
151+ argmap[instance.ip] = instance
152+ argmap[instance.servername] = instance
153+ try:
154+ return (argmap[identifier] for identifier in identifiers)
155+ except KeyError as e:
156+ sys.stderr.write('Cannot find %s\n' % e)
157+ sys.exit(1)
158+
159+
160+def find_host_keys(instances):
161+ ips = [instance.ip for instance in instances]
162+ proc = Popen(
163+ ['ssh', 'chinstrap.canonical.com', 'ssh-keyscan'] + ips, stdout=PIPE,
164+ stderr=PIPE)
165+ keys = proc.communicate()[0]
166+ key_map = {}
167+ for key in keys.splitlines(True):
168+ key_map[key.split(' ')[0]] = key
169+ for instance in instances:
170+ instance.host_key = key_map[instance.ip]
171+
172+
173+def get_key_fingerprint(key):
174+ with NamedTemporaryFile() as f:
175+ f.write(key)
176+ f.flush()
177+ proc = Popen(['ssh-keygen', '-lf', f.name], stdout=PIPE)
178+ host_fingerprint = proc.communicate()[0]
179+ return host_fingerprint.split(' ')[1]
180+
181+
182+def remove_key(known_hosts, hostname):
183+ proc = Popen(['ssh-keygen', '-R', hostname, '-f', known_hosts],
184+ stderr=PIPE)
185+ proc.communicate()
186+
187+
188+def update_key(instance):
189+ known_hosts = os.path.join(os.environ['HOME'], '.ssh/known_hosts')
190+ hostnames = [instance.ip, instance.servername,
191+ instance.servername + '.canonistack']
192+ for name in hostnames:
193+ remove_key(known_hosts, name)
194+ with file(known_hosts, 'ab') as f:
195+ for name in hostnames:
196+ f.write(instance.host_key.replace(instance.ip, name))
197+
198+def validate_instances(instances):
199+ for instance in instances:
200+ host_fingerprint = get_key_fingerprint(instance.host_key)
201+ yield instance, bool(host_fingerprint == instance.instance_fingerprint)
202+
203+if __name__ == '__main__':
204+ parser = OptionParser()
205+ parser.add_option('--poll', action='store_true')
206+ options, identifiers = parser.parse_args(sys.argv[1:])
207+
208+ if len(identifiers) > 0:
209+ instances = list(get_argmap(identifiers))
210+ else:
211+ instances = list(iter_instance())
212+
213+ for instance in instances:
214+ if options.poll:
215+ attempt_count = 10
216+ else:
217+ attempt_count = 1
218+ for attempt in range(attempt_count):
219+ if attempt != 0:
220+ sys.stdout.write('.')
221+ sys.stdout.flush()
222+ time.sleep(5)
223+ instance.find_fingerprint()
224+ if instance.instance_fingerprint is not None:
225+ break
226+ if attempt != 0:
227+ sys.stdout.write('\n')
228+
229+ if instance.instance_fingerprint is None:
230+ sys.stderr.write('Cannot find key in console output for %s\n' %
231+ instance.instance_id)
232+ sys.exit(1)
233+
234+ find_host_keys(instances)
235+ for instance, valid in validate_instances(instances):
236+ if valid:
237+ print 'Match. Adding key for %s.' % instance.ip
238+ update_key(instance)
239+ else:
240+ print "Mismatch. Not adding key."
241
242=== added file 'testr-remote/list-instance-ids'
243--- testr-remote/list-instance-ids 1970-01-01 00:00:00 +0000
244+++ testr-remote/list-instance-ids 2012-12-11 16:13:28 +0000
245@@ -0,0 +1,2 @@
246+#!/bin/sh
247+euca-describe-instances | grep '^INSTANCE' | cut -f2
248
249=== added file 'testr-remote/list-servers'
250--- testr-remote/list-servers 1970-01-01 00:00:00 +0000
251+++ testr-remote/list-servers 2012-12-11 16:13:28 +0000
252@@ -0,0 +1,2 @@
253+#!/bin/sh
254+euca-describe-instances | grep '^INSTANCE' | cut -f4
255
256=== added directory 'testr-remote/server-locks'
257=== added file 'testr-remote/switch-all'
258--- testr-remote/switch-all 1970-01-01 00:00:00 +0000
259+++ testr-remote/switch-all 2012-12-11 16:13:28 +0000
260@@ -0,0 +1,11 @@
261+#!/bin/sh
262+set -e
263+push_branch=$(bzr config -d $1 push_location|sed 's/bzr+ssh:\/\/bazaar.launchpad.net\//lp:/')
264+revision_id=$(bzr revision-info $1|cut -f2 -d' ')
265+echo $push_branch
266+echo $revision_id
267+servers=$(cat servers)
268+for server in $servers; do
269+ echo Switching $server ...
270+ ssh $server bzr switch -d /opt/launchpad/launchpad $push_branch -r revid:$revision_id
271+done
272
273=== added file 'testr-remote/test-remote'
274--- testr-remote/test-remote 1970-01-01 00:00:00 +0000
275+++ testr-remote/test-remote 2012-12-11 16:13:28 +0000
276@@ -0,0 +1,71 @@
277+#!/usr/bin/env python
278+__metaclass__ = type
279+
280+import argparse
281+import errno
282+import fcntl
283+import os.path
284+from contextlib import contextmanager
285+import re
286+import subprocess
287+import sys
288+import time
289+
290+def escape(iput):
291+ return re.sub('[^-^a-z^A-Z^_.]', lambda m: '\\'+m.group(0), iput)
292+
293+
294+class ServerSet:
295+
296+ def __init__(self, servers):
297+ self.servers = servers
298+
299+ @classmethod
300+ def from_file(cls):
301+ return cls(file('servers').read().splitlines())
302+
303+ @contextmanager
304+ def selected_server(self):
305+ while True:
306+ for server in self.servers:
307+ filename = os.path.join('server-locks', server)
308+ with file(filename, 'wb') as lockfile:
309+ try:
310+ fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
311+ except IOError as e:
312+ if e.errno != errno.EWOULDBLOCK:
313+ raise
314+ else:
315+ try:
316+ yield server
317+ finally:
318+ fcntl.lockf(lockfile, fcntl.LOCK_UN)
319+ return
320+ else:
321+ time.sleep(1)
322+
323+
324+parser = argparse.ArgumentParser(description='Run some tests remotely.',
325+ add_help=False)
326+parser.add_argument('--load-list', nargs=1, type=argparse.FileType())
327+known, unknown = parser.parse_known_args()
328+args = [escape(arg) for arg in unknown]
329+try:
330+ with ServerSet.from_file().selected_server() as server:
331+ command = ['ssh', server]
332+ if known.load_list is not None:
333+ command.extend(['id_list=$(mktemp);',
334+ # 'echo $id_list;',
335+ 'cat > $id_list;'])
336+ args = ['--load-list', '$id_list'] + args
337+ stdin = known.load_list[0]
338+ else:
339+ stdin = None
340+ command.extend(['cd /opt/launchpad/launchpad;', 'xvfb-run',
341+ 'bin/test'])
342+ command.extend(args)
343+ #print command
344+ sys.exit(subprocess.call(command, stdin=stdin))
345+except Exception as e:
346+ sys.stderr.write(str(e) + '\n')
347+ sys.exit(2)
348
349=== added file 'testr-remote/testr-remote'
350--- testr-remote/testr-remote 1970-01-01 00:00:00 +0000
351+++ testr-remote/testr-remote 2012-12-11 16:13:28 +0000
352@@ -0,0 +1,4 @@
353+#!/bin/sh
354+server_count=$(wc -l servers | cut -f 1 -d ' ')
355+concurrency=$((server_count*2))
356+~/hacking/testrepository/testr run --parallel --concurrency $concurrency "$@"
357
358=== added file 'testr-remote/update-all'
359--- testr-remote/update-all 1970-01-01 00:00:00 +0000
360+++ testr-remote/update-all 2012-12-11 16:13:28 +0000
361@@ -0,0 +1,2 @@
362+#!/bin/sh
363+for server in $(cat servers); do ssh $server bzr update /opt/launchpad/launchpad; done

Subscribers

People subscribed via source and target branches