Merge lp:~mwhudson/launchpad/ec2-entrypoints into lp:launchpad

Proposed by Michael Hudson-Doyle
Status: Merged
Merged at revision: not available
Proposed branch: lp:~mwhudson/launchpad/ec2-entrypoints
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~mwhudson/launchpad/ec2-entrypoints
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+12024@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hi Jono,

You've seen this branch before. Since last time:

1) A good degree of tidying, mostly as discussed.
2) ec2 help works in most ways that I think it should.
3) No tests :( The fact that the ec2 stuff only works with 2.4+ bites, basically.

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (37.8 KiB)

Hey Michael,

We've already talked & had an email conversation about this, so I've been brief in my comments. Apologies if it looks like brusqueness.

A fair few questions, so marking as 'needs fixing', even though the general direction is great and the vast majority of the code is fine.

jml

> === added file 'lib/devscripts/ec2test/builtins.py'
> --- lib/devscripts/ec2test/builtins.py 1970-01-01 00:00:00 +0000
> +++ lib/devscripts/ec2test/builtins.py 2009-09-18 03:16:29 +0000
> @@ -0,0 +1,404 @@

Copyright notice, __all__, metaclass thingy.

> +import pdb
> +
> +from bzrlib.commands import Command
> +from bzrlib.errors import BzrCommandError
> +from bzrlib.help import help_commands
> +from bzrlib.option import ListOption, Option
> +
> +import socket
> +
> +from devscripts.ec2test.credentials import EC2Credentials
> +from devscripts.ec2test.instance import (
> + AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE, EC2Instance)
> +from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH
> +
> +

A comment before each of these options explaining what they are for would
probably help future maintainers.

> +branch_option = ListOption(
> + 'branch', type=str, short_name='b', argname='BRANCH',
> + help=('Branches to include in this run in sourcecode. '
> + 'If the argument is only the project name, the trunk will be '
> + 'used (e.g., ``-b launchpadlib``). If you want to use a '
> + 'specific branch, if it is on launchpad, you can usually '
> + 'simply specify it instead (e.g., '
> + '``-b lp:~username/launchpadlib/branchname``). If this does '
> + 'not appear to work, or if the desired branch is not on '
> + 'launchpad, specify the project name and then the branch '
> + 'after an equals sign (e.g., '
> + '``-b launchpadlib=lp:~username/launchpadlib/branchname``). '
> + 'Branches for multiple projects may be specified with '
> + 'multiple instances of this option. '
> + 'You may also use this option to specify the branch of launchpad '
> + 'into which your branch may be merged. This defaults to %s. '
> + 'Because typically the important branches of launchpad are owned '
> + 'by the launchpad-pqm user, you can shorten this to only the '
> + 'branch name, if desired, and the launchpad-pqm user will be '
> + 'assumed. For instance, if you specify '
> + '``-b launchpad=db-devel`` then this is equivalent to '
> + '``-b lp:~launchpad-pqm/launchpad/db-devel``, or the even longer'
> + '``-b launchpad=lp:~launchpad-pqm/launchpad/db-devel``.'
> + % (TRUNK_BRANCH,)))
> +
> +
> +machine_id_option = Option(
> + 'machine', short_name='m', type=str,
> + help=('The AWS machine identifier (AMI) on which to base this run. '
> + 'You should typically only have to supply this if you are '
> + 'testing new AWS images. Defaults to trying to find the most '
> + 'recent one with an approved owner.'))
> +
> +
> +def _convert_instance_type(arg):
> + """Ensure that `arg` is acceptable as an instance type."""
> + if arg not in ...

review: Needs Fixing
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (39.7 KiB)

Jonathan Lange wrote:
> Review: Needs Fixing
> Hey Michael,
>
> We've already talked & had an email conversation about this, so I've been brief in my comments. Apologies if it looks like brusqueness.
>
> A fair few questions, so marking as 'needs fixing', even though the general direction is great and the vast majority of the code is fine.
>
> jml
>
>> === added file 'lib/devscripts/ec2test/builtins.py'
>> --- lib/devscripts/ec2test/builtins.py 1970-01-01 00:00:00 +0000
>> +++ lib/devscripts/ec2test/builtins.py 2009-09-18 03:16:29 +0000
>> @@ -0,0 +1,404 @@
>
> Copyright notice, __all__, metaclass thingy.

Doh!

>> +import pdb
>> +
>> +from bzrlib.commands import Command
>> +from bzrlib.errors import BzrCommandError
>> +from bzrlib.help import help_commands
>> +from bzrlib.option import ListOption, Option
>> +
>> +import socket
>> +
>> +from devscripts.ec2test.credentials import EC2Credentials
>> +from devscripts.ec2test.instance import (
>> + AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE, EC2Instance)
>> +from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH
>> +
>> +
>
> A comment before each of these options explaining what they are for would
> probably help future maintainers.

I added one comment, and beefed up the help strings a bit. I think if
any are still confusing, the help should probably be improved...

>> +branch_option = ListOption(
>> + 'branch', type=str, short_name='b', argname='BRANCH',
>> + help=('Branches to include in this run in sourcecode. '
>> + 'If the argument is only the project name, the trunk will be '
>> + 'used (e.g., ``-b launchpadlib``). If you want to use a '
>> + 'specific branch, if it is on launchpad, you can usually '
>> + 'simply specify it instead (e.g., '
>> + '``-b lp:~username/launchpadlib/branchname``). If this does '
>> + 'not appear to work, or if the desired branch is not on '
>> + 'launchpad, specify the project name and then the branch '
>> + 'after an equals sign (e.g., '
>> + '``-b launchpadlib=lp:~username/launchpadlib/branchname``). '
>> + 'Branches for multiple projects may be specified with '
>> + 'multiple instances of this option. '
>> + 'You may also use this option to specify the branch of launchpad '
>> + 'into which your branch may be merged. This defaults to %s. '
>> + 'Because typically the important branches of launchpad are owned '
>> + 'by the launchpad-pqm user, you can shorten this to only the '
>> + 'branch name, if desired, and the launchpad-pqm user will be '
>> + 'assumed. For instance, if you specify '
>> + '``-b launchpad=db-devel`` then this is equivalent to '
>> + '``-b lp:~launchpad-pqm/launchpad/db-devel``, or the even longer'
>> + '``-b launchpad=lp:~launchpad-pqm/launchpad/db-devel``.'
>> + % (TRUNK_BRANCH,)))
>> +
>> +
>> +machine_id_option = Option(
>> + 'machine', short_name='m', type=str,
>> + help=('The AWS machine identifier (AMI) on which to base this run. '
>> + 'You should typically only have to supply this if you ar...

1=== modified file 'lib/devscripts/ec2test/builtins.py'
2--- lib/devscripts/ec2test/builtins.py 2009-09-18 03:16:29 +0000
3+++ lib/devscripts/ec2test/builtins.py 2009-09-18 05:33:59 +0000
4@@ -1,3 +1,11 @@
5+# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""The command classes for the 'ec2' utility."""
9+
10+__metaclass__ = type
11+__all__ = []
12+
13 import pdb
14
15 from bzrlib.commands import Command
16@@ -13,6 +21,11 @@
17 from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH
18
19
20+# Options accepted by more than one command.
21+
22+# Branches is a complicated option that lets the user specify which branches
23+# to use in the sourcecode directory. Most of the complexity is still in
24+# EC2TestRunner.__init__, which probably isn't ideal.
25 branch_option = ListOption(
26 'branch', type=str, short_name='b', argname='BRANCH',
27 help=('Branches to include in this run in sourcecode. '
28@@ -69,7 +82,8 @@
29
30 trunk_option = Option(
31 'trunk', short_name='t',
32- help=('Run the trunk as the branch'))
33+ help=('Run the trunk as the branch, rather than the branch of the '
34+ 'current working directory.'))
35
36
37 include_download_cache_changes_option = Option(
38@@ -81,6 +95,12 @@
39 'changes in your download cache, you must explicitly choose to '
40 'include or ignore the changes.'))
41
42+postmortem_option = Option(
43+ 'postmortem', short_name='p',
44+ help=('Drop to interactive prompt after the test and before shutting '
45+ 'down the instance for postmortem analysis of the EC2 instance '
46+ 'and/or of this script.'))
47+
48
49 class EC2Command(Command):
50 """Subclass of `Command` that customizes usage to say 'ec2' not 'bzr'.
51@@ -108,6 +128,26 @@
52 s = s[:-1] # remove last space
53 return s
54
55+def _get_branches_and_test_branch(trunk, branch, test_branch):
56+ """Interpret the command line options to find which branch to test.
57+
58+ :param trunk: The value of the --trunk option.
59+ :param branch: The value of the --branch options.
60+ :param test_branch: The value of the TEST_BRANCH argument.
61+ """
62+ if trunk:
63+ if test_branch is not None:
64+ raise BzrCommandError(
65+ "Cannot specify both a branch to test and --trunk")
66+ else:
67+ test_branch = TRUNK_BRANCH
68+ else:
69+ if test_branch is None:
70+ test_branch = '.'
71+ branches = [data.split('=', 1) for data in branch]
72+ return branches, test_branch
73+
74+
75
76 class cmd_test(EC2Command):
77 """Run the test suite in ec2."""
78@@ -163,11 +203,7 @@
79 'If the branch is local, then the bzr configuration is '
80 'consulted; for remote branches "Launchpad PQM '
81 '<launchpad@pqm.canonical.com>" is used by default.')),
82- Option(
83- 'postmortem', short_name='p',
84- help=('Drop to interactive prompt after the test and before shutting '
85- 'down the instance for postmortem analysis of the EC2 instance '
86- 'and/or of this script.')),
87+ postmortem_option,
88 Option(
89 'headless',
90 help=('After building the instance and test, run the remote tests '
91@@ -191,15 +227,8 @@
92 include_download_cache_changes=False):
93 if debug:
94 pdb.set_trace()
95- if trunk:
96- if test_branch is not None:
97- raise BzrCommandError(
98- "Cannot specify both a branch to test and --trunk")
99- else:
100- test_branch = TRUNK_BRANCH
101- else:
102- if test_branch is None:
103- test_branch = '.'
104+ branches, test_branch = _get_branches_and_test_branch(
105+ trunk, branch, test_branch)
106 if ((postmortem or file) and headless):
107 raise BzrCommandError(
108 'Headless mode currently does not support postmortem or file '
109@@ -211,7 +240,6 @@
110 else:
111 if email == []:
112 email = True
113- branches = [data.split('=', 1) for data in branch]
114
115 if headless and not (email or submit_pqm_message):
116 raise BzrCommandError(
117@@ -219,7 +247,7 @@
118 'of your headless test run.')
119
120
121- instance = EC2Instance(
122+ instance = EC2Instance.make(
123 EC2TestRunner.name, instance_type, machine)
124
125 runner = EC2TestRunner(
126@@ -246,11 +274,7 @@
127 trunk_option,
128 machine_id_option,
129 instance_type_option,
130- Option(
131- 'postmortem', short_name='p',
132- help=('Drop to interactive prompt after the test and before shutting '
133- 'down the instance for postmortem analysis of the EC2 instance '
134- 'and/or of this script.')),
135+ postmortem_option,
136 debug_option,
137 include_download_cache_changes_option,
138 ListOption(
139@@ -265,16 +289,8 @@
140 include_download_cache_changes=False, demo=None):
141 if debug:
142 pdb.set_trace()
143- if trunk:
144- if test_branch is not None:
145- raise BzrCommandError(
146- "Cannot specify both a branch to test and --trunk")
147- else:
148- test_branch = TRUNK_BRANCH
149- else:
150- if test_branch is None:
151- test_branch = '.'
152- branches = [data.split('=', 1) for data in branch]
153+ branches, test_branch = _get_branches_and_test_branch(
154+ trunk, branch, test_branch)
155
156 instance = EC2Instance.make(
157 EC2TestRunner.name, instance_type, machine, demo)
158@@ -287,14 +303,17 @@
159 demo_network_string = '\n'.join(
160 ' ' + network for network in demo)
161
162+ # Wait until the user exits the postmortem session, then kill the
163+ # instance.
164+ postmortem = True
165+ shutdown = True
166 instance.set_up_and_run(
167- True, False, self.run_server, runner,
168- instance.hostname, demo_network_string)
169-
170-
171- def run_server(self, runner, hostname, demo_network_string):
172+ postmortem, shutdown, self.run_server, runner,
173+ demo_network_string)
174+
175+ def run_server(self, runner, instance, demo_network_string):
176 runner.run_demo_server()
177- ec2_ip = socket.gethostbyname(hostname)
178+ ec2_ip = socket.gethostbyname(instance.hostname)
179 print (
180 "\n\n"
181 "********************** DEMO *************************\n"
182@@ -303,7 +322,7 @@
183 "network access to the ec2 instance from their IPs by\n"
184 "entering command like this in the interactive python\n"
185 "interpreter at the end of the setup. "
186- "\n instance.security_group.authorize("
187+ "\n self.security_group.authorize("
188 "'tcp', 443, 443, '10.0.0.5/32')\n\n"
189 "These demo networks have already been granted access on "
190 "port 80 and 443:\n" + demo_network_string +
191@@ -323,11 +342,7 @@
192 takes_options = [
193 machine_id_option,
194 instance_type_option,
195- Option(
196- 'postmortem', short_name='p',
197- help=('Drop to interactive prompt after the test and before shutting '
198- 'down the instance for postmortem analysis of the EC2 instance '
199- 'and/or of this script.')),
200+ postmortem_option,
201 debug_option,
202 ListOption(
203 'extra-update-image-command', type=str,
204@@ -356,6 +371,23 @@
205
206 def update_image(self, instance, extra_update_image_command, ami_name,
207 credentials):
208+ """Bring the image up to date.
209+
210+ The steps we take are:
211+
212+ * run any commands specified with --extra-update-image-command
213+ * update sourcecode via rsync.
214+ * update the launchpad branch to the tip of the trunk branch.
215+ * update the copy of the download-cache.
216+ * remove the user account.
217+ * bundle the image
218+
219+ :param instance: `EC2Instance` to operate on.
220+ :param extra_update_image_command: List of commands to run on the
221+ instance in addition to the usual ones.
222+ :param ami_name: The name to give the created AMI.
223+ :param credentials: An `EC2Credentials` object.
224+ """
225 user_connection = instance.connect_as_user()
226 user_connection.perform('bzr launchpad-login %(launchpad-login)s')
227 for cmd in extra_update_image_command:
228@@ -391,14 +423,13 @@
229 to show some basic usage information.
230 """
231 if topic is None:
232- print >>self.outf, 'Usage: ec2 <command> <options>'
233- print >>self.outf
234- print >>self.outf, 'Available commands:'
235+ self.outf.write('Usage: ec2 <command> <options>\n\n')
236+ self.outf.write('Available commands:\n')
237 help_commands(self.outf)
238 else:
239 command = self.controller._get_command(None, topic)
240 if command is None:
241- print >>self.outf, "%s is an unknown command." % (topic,)
242+ self.outf.write("%s is an unknown command.\n" % (topic,))
243 text = command.get_help_text()
244 if text:
245- print >>self.outf, text
246+ self.outf.write(text)
247
248=== modified file 'lib/devscripts/ec2test/entrypoint.py'
249--- lib/devscripts/ec2test/entrypoint.py 2009-09-18 03:10:25 +0000
250+++ lib/devscripts/ec2test/entrypoint.py 2009-09-18 05:35:18 +0000
251@@ -1,3 +1,13 @@
252+# Copyright 2009 Canonical Ltd. This software is licensed under the
253+# GNU Affero General Public License version 3 (see the file LICENSE).
254+
255+"""The entry point for the 'ec2' utility."""
256+
257+__metaclass__ = type
258+__all__ = [
259+ 'main',
260+ ]
261+
262 import readline
263 import rlcompleter
264 import sys
265@@ -14,11 +24,14 @@
266 readline.parse_and_bind('tab: complete')
267
268 class EC2CommandController(CommandRegistry, CommandExecutionMixin):
269- def __init__(self):
270- CommandRegistry.__init__(self)
271+ """The 'ec2' utility registers and executes commands."""
272
273
274 def main():
275+ """The entry point for the 'ec2' script.
276+
277+ We run the specified command, or give help if none was specified.
278+ """
279 controller = EC2CommandController()
280 controller.install_bzrlib_hooks()
281 controller.load_module(builtins)
282
283=== modified file 'lib/devscripts/ec2test/instance.py'
284--- lib/devscripts/ec2test/instance.py 2009-09-18 02:23:13 +0000
285+++ lib/devscripts/ec2test/instance.py 2009-09-18 05:25:50 +0000
286@@ -26,6 +26,7 @@
287 from devscripts.ec2test.sshconfig import SSHConfig
288 from devscripts.ec2test.credentials import EC2Credentials
289
290+
291 DEFAULT_INSTANCE_TYPE = 'c1.xlarge'
292 AVAILABLE_INSTANCE_TYPES = ('m1.large', 'm1.xlarge', 'c1.xlarge')
293
294@@ -40,7 +41,7 @@
295
296
297 def get_user_key():
298- """Get a SSH key from the agent. Exit if not found.
299+ """Get a SSH key from the agent. Raise an error if not found.
300
301 This key will be used to let the user log in (as $USER) to the instance.
302 """
303@@ -68,8 +69,8 @@
304 :param instance_type: One of the AVAILABLE_INSTANCE_TYPES.
305 :param machine_id: The AMI to use, or None to do the usual regexp
306 matching.
307- :param demo_networks: The networks to add to the security group to
308- allow access to the instance.
309+ :param demo_networks: A list of networks to add to the security group
310+ to allow access to the instance.
311 :param credentials: An `EC2Credentials` object.
312 """
313 if instance_type not in AVAILABLE_INSTANCE_TYPES:
314@@ -267,12 +268,12 @@
315 root_connection.close()
316
317 def set_up_and_run(self, postmortem, shutdown, func, *args, **kw):
318- """Start and set up, run `func` and then optionally shut down.
319+ """Start, set up a user account, run `func` and then maybe shut down.
320
321 :param postmortem: If true, any exceptions will be caught and an
322 interactive session run to allow debugging the problem.
323- :param shutdown: If true, the instance will be shut down before this
324- function returns.
325+ :param shutdown: If true, shut down the instance after `func` and
326+ postmortem (if any) are completed.
327 :param func: A callable that will be called when the instance is
328 running and a user account has been set up on it.
329 :param args: Passed to `func`.
Revision history for this message
Jonathan Lange (jml) wrote :

You have three blank lines after _get_branches_and_test_branch, rather than the regulation two. Other than that, it looks great. Well done!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/devscripts/ec2test/__init__.py'
2--- lib/devscripts/ec2test/__init__.py 2009-09-14 05:43:04 +0000
3+++ lib/devscripts/ec2test/__init__.py 2009-09-18 02:23:13 +0000
4@@ -5,13 +5,11 @@
5
6 __metaclass__ = type
7
8-__all__ = [
9- 'error_and_quit',
10- 'main',
11- ]
12+__all__ = []
13
14+from bzrlib.plugin import load_plugins
15+load_plugins()
16 import paramiko
17-import sys
18
19 #############################################################################
20 # Try to guide users past support problems we've encountered before
21@@ -21,11 +19,3 @@
22 # maybe add similar check for bzrlib?
23 # End
24 #############################################################################
25-
26-def error_and_quit(msg):
27- """Print error message and exit."""
28- sys.stderr.write(msg)
29- sys.exit(1)
30-
31-from devscripts.ec2test.commandline import main
32-main # shut up pyflakes
33
34=== added file 'lib/devscripts/ec2test/builtins.py'
35--- lib/devscripts/ec2test/builtins.py 1970-01-01 00:00:00 +0000
36+++ lib/devscripts/ec2test/builtins.py 2009-09-18 03:16:29 +0000
37@@ -0,0 +1,404 @@
38+import pdb
39+
40+from bzrlib.commands import Command
41+from bzrlib.errors import BzrCommandError
42+from bzrlib.help import help_commands
43+from bzrlib.option import ListOption, Option
44+
45+import socket
46+
47+from devscripts.ec2test.credentials import EC2Credentials
48+from devscripts.ec2test.instance import (
49+ AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE, EC2Instance)
50+from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH
51+
52+
53+branch_option = ListOption(
54+ 'branch', type=str, short_name='b', argname='BRANCH',
55+ help=('Branches to include in this run in sourcecode. '
56+ 'If the argument is only the project name, the trunk will be '
57+ 'used (e.g., ``-b launchpadlib``). If you want to use a '
58+ 'specific branch, if it is on launchpad, you can usually '
59+ 'simply specify it instead (e.g., '
60+ '``-b lp:~username/launchpadlib/branchname``). If this does '
61+ 'not appear to work, or if the desired branch is not on '
62+ 'launchpad, specify the project name and then the branch '
63+ 'after an equals sign (e.g., '
64+ '``-b launchpadlib=lp:~username/launchpadlib/branchname``). '
65+ 'Branches for multiple projects may be specified with '
66+ 'multiple instances of this option. '
67+ 'You may also use this option to specify the branch of launchpad '
68+ 'into which your branch may be merged. This defaults to %s. '
69+ 'Because typically the important branches of launchpad are owned '
70+ 'by the launchpad-pqm user, you can shorten this to only the '
71+ 'branch name, if desired, and the launchpad-pqm user will be '
72+ 'assumed. For instance, if you specify '
73+ '``-b launchpad=db-devel`` then this is equivalent to '
74+ '``-b lp:~launchpad-pqm/launchpad/db-devel``, or the even longer'
75+ '``-b launchpad=lp:~launchpad-pqm/launchpad/db-devel``.'
76+ % (TRUNK_BRANCH,)))
77+
78+
79+machine_id_option = Option(
80+ 'machine', short_name='m', type=str,
81+ help=('The AWS machine identifier (AMI) on which to base this run. '
82+ 'You should typically only have to supply this if you are '
83+ 'testing new AWS images. Defaults to trying to find the most '
84+ 'recent one with an approved owner.'))
85+
86+
87+def _convert_instance_type(arg):
88+ """Ensure that `arg` is acceptable as an instance type."""
89+ if arg not in AVAILABLE_INSTANCE_TYPES:
90+ raise BzrCommandError('Unknown instance type %r' % arg)
91+ return arg
92+
93+
94+instance_type_option = Option(
95+ 'instance', short_name='i', type=_convert_instance_type,
96+ param_name='instance_type',
97+ help=('The AWS instance type on which to base this run. '
98+ 'Available options are %r. Defaults to `%s`.' %
99+ (AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE)))
100+
101+
102+debug_option = Option(
103+ 'debug', short_name='d',
104+ help=('Drop to pdb trace as soon as possible.'))
105+
106+
107+trunk_option = Option(
108+ 'trunk', short_name='t',
109+ help=('Run the trunk as the branch'))
110+
111+
112+include_download_cache_changes_option = Option(
113+ 'include-download-cache-changes', short_name='c',
114+ help=('Include any changes in the download cache (added or unknown) '
115+ 'in the download cache of the test run. Note that, if you have '
116+ 'any changes in your download cache, trying to submit to pqm '
117+ 'will always raise an error. Also note that, if you have any '
118+ 'changes in your download cache, you must explicitly choose to '
119+ 'include or ignore the changes.'))
120+
121+
122+class EC2Command(Command):
123+ """Subclass of `Command` that customizes usage to say 'ec2' not 'bzr'.
124+
125+ When https://bugs.edge.launchpad.net/bzr/+bug/431054 is fixed, we can
126+ delete this class, or at least make it less of a copy/paste/hack of the
127+ superclass.
128+ """
129+
130+ def _usage(self):
131+ """Return single-line grammar for this command.
132+
133+ Only describes arguments, not options.
134+ """
135+ s = 'ec2 ' + self.name() + ' '
136+ for aname in self.takes_args:
137+ aname = aname.upper()
138+ if aname[-1] in ['$', '+']:
139+ aname = aname[:-1] + '...'
140+ elif aname[-1] == '?':
141+ aname = '[' + aname[:-1] + ']'
142+ elif aname[-1] == '*':
143+ aname = '[' + aname[:-1] + '...]'
144+ s += aname + ' '
145+ s = s[:-1] # remove last space
146+ return s
147+
148+
149+class cmd_test(EC2Command):
150+ """Run the test suite in ec2."""
151+
152+ takes_options = [
153+ branch_option,
154+ trunk_option,
155+ machine_id_option,
156+ instance_type_option,
157+ Option(
158+ 'file', short_name='f',
159+ help=('Store abridged test results in FILE.')),
160+ ListOption(
161+ 'email', short_name='e', argname='EMAIL', type=str,
162+ help=('Email address to which results should be mailed. Defaults to '
163+ 'the email address from `bzr whoami`. May be supplied multiple '
164+ 'times. The first supplied email address will be used as the '
165+ 'From: address.')),
166+ Option(
167+ 'noemail', short_name='n',
168+ help=('Do not try to email results.')),
169+ Option(
170+ 'test-options', short_name='o', type=str,
171+ help=('Test options to pass to the remote test runner. Defaults to '
172+ "``-o '-vv'``. For instance, to run specific tests, you might "
173+ "use ``-o '-vvt my_test_pattern'``.")),
174+ Option(
175+ 'submit-pqm-message', short_name='s', type=str, argname="MSG",
176+ help=('A pqm message to submit if the test run is successful. If '
177+ 'provided, you will be asked for your GPG passphrase before '
178+ 'the test run begins.')),
179+ Option(
180+ 'pqm-public-location', type=str,
181+ help=('The public location for the pqm submit, if a pqm message is '
182+ 'provided (see --submit-pqm-message). If this is not provided, '
183+ 'for local branches, bzr configuration is consulted; for '
184+ 'remote branches, it is assumed that the remote branch *is* '
185+ 'a public branch.')),
186+ Option(
187+ 'pqm-submit-location', type=str,
188+ help=('The submit location for the pqm submit, if a pqm message is '
189+ 'provided (see --submit-pqm-message). If this option is not '
190+ 'provided, the script will look for an explicitly specified '
191+ 'launchpad branch using the -b/--branch option; if that branch '
192+ 'was specified and is owned by the launchpad-pqm user on '
193+ 'launchpad, it is used as the pqm submit location. Otherwise, '
194+ 'for local branches, bzr configuration is consulted; for '
195+ 'remote branches, it is assumed that the submit branch is %s.'
196+ % (TRUNK_BRANCH,))),
197+ Option(
198+ 'pqm-email', type=str,
199+ help=('Specify the email address of the PQM you are submitting to. '
200+ 'If the branch is local, then the bzr configuration is '
201+ 'consulted; for remote branches "Launchpad PQM '
202+ '<launchpad@pqm.canonical.com>" is used by default.')),
203+ Option(
204+ 'postmortem', short_name='p',
205+ help=('Drop to interactive prompt after the test and before shutting '
206+ 'down the instance for postmortem analysis of the EC2 instance '
207+ 'and/or of this script.')),
208+ Option(
209+ 'headless',
210+ help=('After building the instance and test, run the remote tests '
211+ 'headless. Cannot be used with postmortem '
212+ 'or file.')),
213+ debug_option,
214+ Option(
215+ 'open-browser',
216+ help=('Open the results page in your default browser')),
217+ include_download_cache_changes_option,
218+ ]
219+
220+ takes_args = ['test_branch?']
221+
222+ def run(self, test_branch=None, branch=[], trunk=False, machine=None,
223+ instance_type=DEFAULT_INSTANCE_TYPE,
224+ file=None, email=None, test_options='-vv', noemail=False,
225+ submit_pqm_message=None, pqm_public_location=None,
226+ pqm_submit_location=None, pqm_email=None, postmortem=False,
227+ headless=False, debug=False, open_browser=False,
228+ include_download_cache_changes=False):
229+ if debug:
230+ pdb.set_trace()
231+ if trunk:
232+ if test_branch is not None:
233+ raise BzrCommandError(
234+ "Cannot specify both a branch to test and --trunk")
235+ else:
236+ test_branch = TRUNK_BRANCH
237+ else:
238+ if test_branch is None:
239+ test_branch = '.'
240+ if ((postmortem or file) and headless):
241+ raise BzrCommandError(
242+ 'Headless mode currently does not support postmortem or file '
243+ ' options.')
244+ if noemail:
245+ if email:
246+ raise BzrCommandError(
247+ 'May not supply both --no-email and an --email address')
248+ else:
249+ if email == []:
250+ email = True
251+ branches = [data.split('=', 1) for data in branch]
252+
253+ if headless and not (email or submit_pqm_message):
254+ raise BzrCommandError(
255+ 'You have specified no way to get the results '
256+ 'of your headless test run.')
257+
258+
259+ instance = EC2Instance(
260+ EC2TestRunner.name, instance_type, machine)
261+
262+ runner = EC2TestRunner(
263+ test_branch, email=email, file=file,
264+ test_options=test_options, headless=headless,
265+ branches=branches, pqm_message=submit_pqm_message,
266+ pqm_public_location=pqm_public_location,
267+ pqm_submit_location=pqm_submit_location,
268+ open_browser=open_browser, pqm_email=pqm_email,
269+ include_download_cache_changes=include_download_cache_changes,
270+ instance=instance, vals=instance._vals)
271+
272+ instance.set_up_and_run(postmortem, not headless, runner.run_tests)
273+
274+
275+class cmd_demo(EC2Command):
276+ """Start a demo instance of Launchpad.
277+
278+ See https://wiki.canonical.com/Launchpad/EC2Test/ForDemos
279+ """
280+
281+ takes_options = [
282+ branch_option,
283+ trunk_option,
284+ machine_id_option,
285+ instance_type_option,
286+ Option(
287+ 'postmortem', short_name='p',
288+ help=('Drop to interactive prompt after the test and before shutting '
289+ 'down the instance for postmortem analysis of the EC2 instance '
290+ 'and/or of this script.')),
291+ debug_option,
292+ include_download_cache_changes_option,
293+ ListOption(
294+ 'demo', type=str,
295+ help="Allow this netmask to connect to the instance."),
296+ ]
297+
298+ takes_args = ['test_branch?']
299+
300+ def run(self, test_branch=None, branch=[], trunk=False, machine=None,
301+ instance_type=DEFAULT_INSTANCE_TYPE, debug=False,
302+ include_download_cache_changes=False, demo=None):
303+ if debug:
304+ pdb.set_trace()
305+ if trunk:
306+ if test_branch is not None:
307+ raise BzrCommandError(
308+ "Cannot specify both a branch to test and --trunk")
309+ else:
310+ test_branch = TRUNK_BRANCH
311+ else:
312+ if test_branch is None:
313+ test_branch = '.'
314+ branches = [data.split('=', 1) for data in branch]
315+
316+ instance = EC2Instance.make(
317+ EC2TestRunner.name, instance_type, machine, demo)
318+
319+ runner = EC2TestRunner(
320+ test_branch, branches=branches,
321+ include_download_cache_changes=include_download_cache_changes,
322+ instance=instance, vals=instance._vals)
323+
324+ demo_network_string = '\n'.join(
325+ ' ' + network for network in demo)
326+
327+ instance.set_up_and_run(
328+ True, False, self.run_server, runner,
329+ instance.hostname, demo_network_string)
330+
331+
332+ def run_server(self, runner, hostname, demo_network_string):
333+ runner.run_demo_server()
334+ ec2_ip = socket.gethostbyname(hostname)
335+ print (
336+ "\n\n"
337+ "********************** DEMO *************************\n"
338+ "It may take 20 seconds for the demo server to start up."
339+ "\nTo demo to other users, you still need to open up\n"
340+ "network access to the ec2 instance from their IPs by\n"
341+ "entering command like this in the interactive python\n"
342+ "interpreter at the end of the setup. "
343+ "\n instance.security_group.authorize("
344+ "'tcp', 443, 443, '10.0.0.5/32')\n\n"
345+ "These demo networks have already been granted access on "
346+ "port 80 and 443:\n" + demo_network_string +
347+ "\n\nYou also need to edit your /etc/hosts to point\n"
348+ "launchpad.dev at the ec2 instance's IP like this:\n"
349+ " " + ec2_ip + " launchpad.dev\n\n"
350+ "See "
351+ "<https://wiki.canonical.com/Launchpad/EC2Test/ForDemos>."
352+ "\n*****************************************************"
353+ "\n\n")
354+
355+
356+
357+class cmd_update_image(EC2Command):
358+ """Make a new AMI."""
359+
360+ takes_options = [
361+ machine_id_option,
362+ instance_type_option,
363+ Option(
364+ 'postmortem', short_name='p',
365+ help=('Drop to interactive prompt after the test and before shutting '
366+ 'down the instance for postmortem analysis of the EC2 instance '
367+ 'and/or of this script.')),
368+ debug_option,
369+ ListOption(
370+ 'extra-update-image-command', type=str,
371+ help=('Run this command (with an ssh agent) on the image before '
372+ 'running the default update steps. Can be passed more than '
373+ 'once, the commands will be run in the order specified.')),
374+ ]
375+
376+ takes_args = ['ami_name']
377+
378+ def run(self, ami_name, machine=None, instance_type='m1.large',
379+ debug=False, postmortem=False, extra_update_image_command=[]):
380+ if debug:
381+ pdb.set_trace()
382+
383+ credentials = EC2Credentials.load_from_file()
384+
385+ instance = EC2Instance.make(
386+ EC2TestRunner.name, instance_type, machine,
387+ credentials=credentials)
388+ instance.check_bundling_prerequisites()
389+
390+ instance.set_up_and_run(
391+ postmortem, True, self.update_image, instance,
392+ extra_update_image_command, ami_name, credentials)
393+
394+ def update_image(self, instance, extra_update_image_command, ami_name,
395+ credentials):
396+ user_connection = instance.connect_as_user()
397+ user_connection.perform('bzr launchpad-login %(launchpad-login)s')
398+ for cmd in extra_update_image_command:
399+ user_connection.run_with_ssh_agent(cmd)
400+ user_connection.run_with_ssh_agent(
401+ "rsync -avp --partial --delete "
402+ "--filter='P *.o' --filter='P *.pyc' --filter='P *.so' "
403+ "devpad.canonical.com:/code/rocketfuel-built/launchpad/sourcecode/* "
404+ "/var/launchpad/sourcecode/")
405+ user_connection.run_with_ssh_agent(
406+ 'bzr pull -d /var/launchpad/test ' + TRUNK_BRANCH)
407+ user_connection.run_with_ssh_agent(
408+ 'bzr pull -d /var/launchpad/download-cache lp:lp-source-dependencies')
409+ user_connection.close()
410+ root_connection = instance.connect_as_root()
411+ root_connection.perform(
412+ 'deluser --remove-home %(USER)s', ignore_failure=True)
413+ root_connection.close()
414+ instance.bundle(ami_name, credentials)
415+
416+
417+class cmd_help(EC2Command):
418+ """Show general help or help for a command."""
419+
420+ aliases = ["?", "--help", "-?", "-h"]
421+ takes_args = ["topic?"]
422+
423+ def run(self, topic=None):
424+ """
425+ Show help for the C{bzrlib.commands.Command} matching C{topic}.
426+
427+ @param topic: Optionally, the name of the topic to show. Default is
428+ to show some basic usage information.
429+ """
430+ if topic is None:
431+ print >>self.outf, 'Usage: ec2 <command> <options>'
432+ print >>self.outf
433+ print >>self.outf, 'Available commands:'
434+ help_commands(self.outf)
435+ else:
436+ command = self.controller._get_command(None, topic)
437+ if command is None:
438+ print >>self.outf, "%s is an unknown command." % (topic,)
439+ text = command.get_help_text()
440+ if text:
441+ print >>self.outf, text
442
443=== removed file 'lib/devscripts/ec2test/commandline.py'
444--- lib/devscripts/ec2test/commandline.py 2009-09-15 01:37:01 +0000
445+++ lib/devscripts/ec2test/commandline.py 1970-01-01 00:00:00 +0000
446@@ -1,369 +0,0 @@
447-# Copyright 2009 Canonical Ltd. This software is licensed under the
448-# GNU Affero General Public License version 3 (see the file LICENSE).
449-
450-"""The command line parsing and entrypoint for ec2test."""
451-
452-__metaclass__ = type
453-__all__ = [
454- 'main',
455- ]
456-
457-import code
458-import optparse
459-import socket
460-import traceback
461-# The rlcompleter and readline modules change the behavior of the python
462-# interactive interpreter just by being imported.
463-import readline
464-import rlcompleter
465-# Shut up pyflakes.
466-rlcompleter
467-
468-import paramiko
469-
470-from devscripts.ec2test import error_and_quit
471-from devscripts.ec2test.credentials import CredentialsError, EC2Credentials
472-from devscripts.ec2test.instance import (
473- AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE, EC2Instance)
474-from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH
475-
476-readline.parse_and_bind('tab: complete')
477-
478-
479-# XXX: JonathanLange 2009-05-31: Strongly considering turning this into a
480-# Bazaar plugin -- probably would make the option parsing and validation
481-# easier.
482-
483-def run_with_instance(instance, run, demo_networks, postmortem):
484- """Call run(), then allow post mortem debugging and shut down `instance`.
485-
486- :param instance: A running `EC2Instance`. If `run` returns True, it will
487- be shut down before this function returns.
488- :param run: A callable that will be called with no arguments to do
489- whatever needs to be done with the instance.
490- :param demo_networks: ???
491- :param postmortem: If this flag is true, any exceptions will be caught and
492- an interactive session run to allow debugging the problem.
493- """
494- shutdown = True
495- try:
496- try:
497- shutdown = run()
498- except Exception:
499- # If we are running in demo or postmortem mode, it is really
500- # helpful to see if there are any exceptions before it waits
501- # in the console (in the finally block), and you can't figure
502- # out why it's broken.
503- traceback.print_exc()
504- finally:
505- try:
506- if demo_networks:
507- demo_network_string = '\n'.join(
508- ' ' + network for network in demo_networks)
509- ec2_ip = socket.gethostbyname(instance.hostname)
510- print (
511- "\n\n"
512- "********************** DEMO *************************\n"
513- "It may take 20 seconds for the demo server to start up."
514- "\nTo demo to other users, you still need to open up\n"
515- "network access to the ec2 instance from their IPs by\n"
516- "entering command like this in the interactive python\n"
517- "interpreter at the end of the setup. "
518- "\n runner.security_group.authorize("
519- "'tcp', 443, 443, '10.0.0.5/32')\n\n"
520- "These demo networks have already been granted access on "
521- "port 80 and 443:\n" + demo_network_string +
522- "\n\nYou also need to edit your /etc/hosts to point\n"
523- "launchpad.dev at the ec2 instance's IP like this:\n"
524- " " + ec2_ip + " launchpad.dev\n\n"
525- "See "
526- "<https://wiki.canonical.com/Launchpad/EC2Test/ForDemos>."
527- "\n*****************************************************"
528- "\n\n")
529- if postmortem:
530- console = code.InteractiveConsole(locals())
531- console.interact((
532- 'Postmortem Console. EC2 instance is not yet dead.\n'
533- 'It will shut down when you exit this prompt (CTRL-D).\n'
534- '\n'
535- 'Tab-completion is enabled.'
536- '\n'
537- 'Test runner instance is available as `runner`.\n'
538- 'Also try these:\n'
539- ' http://%(dns)s/current_test.log\n'
540- ' ssh -A %(dns)s') %
541- {'dns': instance.hostname})
542- print 'Postmortem console closed.'
543- finally:
544- if shutdown:
545- instance.shutdown()
546-
547-
548-def main():
549- parser = optparse.OptionParser(
550- usage="%prog [options] [branch]",
551- description=(
552- "Check out a Launchpad branch and run all tests on an Amazon "
553- "EC2 instance."))
554- parser.add_option(
555- '-f', '--file', dest='file', default=None,
556- help=('Store abridged test results in FILE.'))
557- parser.add_option(
558- '-n', '--no-email', dest='no_email', default=False,
559- action='store_true',
560- help=('Do not try to email results.'))
561- parser.add_option(
562- '-e', '--email', action='append', dest='email', default=None,
563- help=('Email address to which results should be mailed. Defaults to '
564- 'the email address from `bzr whoami`. May be supplied multiple '
565- 'times. The first supplied email address will be used as the '
566- 'From: address.'))
567- parser.add_option(
568- '-o', '--test-options', dest='test_options', default='-vv',
569- help=('Test options to pass to the remote test runner. Defaults to '
570- "``-o '-vv'``. For instance, to run specific tests, you might "
571- "use ``-o '-vvt my_test_pattern'``."))
572- parser.add_option(
573- '-b', '--branch', action='append', dest='branches',
574- help=('Branches to include in this run in sourcecode. '
575- 'If the argument is only the project name, the trunk will be '
576- 'used (e.g., ``-b launchpadlib``). If you want to use a '
577- 'specific branch, if it is on launchpad, you can usually '
578- 'simply specify it instead (e.g., '
579- '``-b lp:~username/launchpadlib/branchname``). If this does '
580- 'not appear to work, or if the desired branch is not on '
581- 'launchpad, specify the project name and then the branch '
582- 'after an equals sign (e.g., '
583- '``-b launchpadlib=lp:~username/launchpadlib/branchname``). '
584- 'Branches for multiple projects may be specified with '
585- 'multiple instances of this option. '
586- 'You may also use this option to specify the branch of launchpad '
587- 'into which your branch may be merged. This defaults to %s. '
588- 'Because typically the important branches of launchpad are owned '
589- 'by the launchpad-pqm user, you can shorten this to only the '
590- 'branch name, if desired, and the launchpad-pqm user will be '
591- 'assumed. For instance, if you specify '
592- '``-b launchpad=db-devel`` then this is equivalent to '
593- '``-b lp:~launchpad-pqm/launchpad/db-devel``, or the even longer'
594- '``-b launchpad=lp:~launchpad-pqm/launchpad/db-devel``.'
595- % (TRUNK_BRANCH,)))
596- parser.add_option(
597- '-t', '--trunk', dest='trunk', default=False,
598- action='store_true',
599- help=('Run the trunk as the branch'))
600- parser.add_option(
601- '-s', '--submit-pqm-message', dest='pqm_message', default=None,
602- help=('A pqm message to submit if the test run is successful. If '
603- 'provided, you will be asked for your GPG passphrase before '
604- 'the test run begins.'))
605- parser.add_option(
606- '--pqm-public-location', dest='pqm_public_location', default=None,
607- help=('The public location for the pqm submit, if a pqm message is '
608- 'provided (see --submit-pqm-message). If this is not provided, '
609- 'for local branches, bzr configuration is consulted; for '
610- 'remote branches, it is assumed that the remote branch *is* '
611- 'a public branch.'))
612- parser.add_option(
613- '--pqm-submit-location', dest='pqm_submit_location', default=None,
614- help=('The submit location for the pqm submit, if a pqm message is '
615- 'provided (see --submit-pqm-message). If this option is not '
616- 'provided, the script will look for an explicitly specified '
617- 'launchpad branch using the -b/--branch option; if that branch '
618- 'was specified and is owned by the launchpad-pqm user on '
619- 'launchpad, it is used as the pqm submit location. Otherwise, '
620- 'for local branches, bzr configuration is consulted; for '
621- 'remote branches, it is assumed that the submit branch is %s.'
622- % (TRUNK_BRANCH,)))
623- parser.add_option(
624- '--pqm-email', dest='pqm_email', default=None,
625- help=('Specify the email address of the PQM you are submitting to. '
626- 'If the branch is local, then the bzr configuration is '
627- 'consulted; for remote branches "Launchpad PQM '
628- '<launchpad@pqm.canonical.com>" is used by default.'))
629- parser.add_option(
630- '-m', '--machine', dest='machine_id', default=None,
631- help=('The AWS machine identifier (AMID) on which to base this run. '
632- 'You should typically only have to supply this if you are '
633- 'testing new AWS images. Defaults to trying to find the most '
634- 'recent one with an approved owner.'))
635- parser.add_option(
636- '-i', '--instance', dest='instance_type',
637- default=DEFAULT_INSTANCE_TYPE,
638- help=('The AWS instance type on which to base this run. '
639- 'Available options are %r. Defaults to `%s`.' %
640- (AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE)))
641- parser.add_option(
642- '-p', '--postmortem', dest='postmortem', default=False,
643- action='store_true',
644- help=('Drop to interactive prompt after the test and before shutting '
645- 'down the instance for postmortem analysis of the EC2 instance '
646- 'and/or of this script.'))
647- parser.add_option(
648- '--headless', dest='headless', default=False,
649- action='store_true',
650- help=('After building the instance and test, run the remote tests '
651- 'headless. Cannot be used with postmortem '
652- 'or file.'))
653- parser.add_option(
654- '-d', '--debug', dest='debug', default=False,
655- action='store_true',
656- help=('Drop to pdb trace as soon as possible.'))
657- # Use tabs to force a newline in the help text.
658- fake_newline = "\t\t\t\t\t\t\t"
659- parser.add_option(
660- '--demo', action='append', dest='demo_networks',
661- help=("Don't run tests. Instead start a demo instance of Launchpad. "
662- "You can allow multiple networks to access the demo by "
663- "repeating the argument." + fake_newline +
664- "Example: --demo 192.168.1.100 --demo 10.1.13.0/24" +
665- fake_newline +
666- "See" + fake_newline +
667- "https://wiki.canonical.com/Launchpad/EC2Test/ForDemos" ))
668- parser.add_option(
669- '--open-browser', dest='open_browser', default=False,
670- action='store_true',
671- help=('Open the results page in your default browser'))
672- parser.add_option(
673- '-c', '--include-download-cache-changes',
674- dest='include_download_cache_changes', action='store_true',
675- help=('Include any changes in the download cache (added or unknown) '
676- 'in the download cache of the test run. Note that, if you have '
677- 'any changes in your download cache, trying to submit to pqm '
678- 'will always raise an error. Also note that, if you have any '
679- 'changes in your download cache, you must explicitly choose to '
680- 'include or ignore the changes.'))
681- parser.add_option(
682- '-g', '--ignore-download-cache-changes',
683- dest='include_download_cache_changes', action='store_false',
684- help=('Ignore any changes in the download cache (added or unknown) '
685- 'in the download cache of the test run. Note that, if you have '
686- 'any changes in your download cache, trying to submit to pqm '
687- 'will always raise an error. Also note that, if you have any '
688- 'changes in your download cache, you must explicitly choose to '
689- 'include or ignore the changes.'))
690- parser.add_option(
691- '--update-image', dest='bundle', action='store',
692- help=('Start the image, update the system packages, sourcecode and '
693- 'Launchpad branch then bundle, upload and register a new AMI '
694- 'with the given name.'))
695- parser.add_option(
696- '--extra-update-image-command', dest='extra_update_image_commands',
697- action='append', metavar="CMD",
698- help=('Run this command (with an ssh agent) on the image before '
699- 'running the default update steps. Can be passed more than '
700- 'once, the commands will be run in the order specified.'))
701- options, args = parser.parse_args()
702- if options.debug:
703- import pdb; pdb.set_trace()
704- if options.demo_networks:
705- # We need the postmortem console to open the ec2 instance's
706- # network access, and to keep the ec2 instance from being shutdown.
707- options.postmortem = True
708- if len(args) == 1:
709- if options.trunk:
710- parser.error(
711- 'Cannot supply both a branch and the --trunk argument.')
712- branch = args[0]
713- elif len(args) > 1:
714- parser.error('Too many arguments.')
715- elif options.trunk:
716- branch = None
717- else:
718- branch = '.'
719- if ((options.postmortem or options.file or options.demo_networks)
720- and options.headless):
721- parser.error(
722- 'Headless mode currently does not support postmortem, file '
723- 'or demo options.')
724- if options.no_email:
725- if options.email:
726- parser.error(
727- 'May not supply both --no-email and an --email address')
728- email = False
729- else:
730- email = options.email
731- if email is None:
732- email = True
733- if options.instance_type not in AVAILABLE_INSTANCE_TYPES:
734- parser.error('Unknown instance type.')
735- if options.branches is None:
736- branches = ()
737- else:
738- branches = [data.split('=', 1) for data in options.branches]
739-
740- agent = paramiko.Agent()
741- keys = agent.get_keys()
742- if len(keys) == 0:
743- error_and_quit(
744- 'You must have an ssh agent running with keys installed that '
745- 'will allow the script to rsync to devpad and get your '
746- 'branch.\n')
747- user_key = agent.get_keys()[0]
748-
749- if options.demo_networks is None:
750- demo_networks = ()
751- else:
752- demo_networks = options.demo_networks
753-
754- # Get the AWS identifier and secret identifier.
755- try:
756- credentials = EC2Credentials.load_from_file()
757- except CredentialsError, e:
758- error_and_quit(str(e))
759-
760- instance = EC2Instance.make(
761- credentials, EC2TestRunner.name, instance_type=options.instance_type,
762- machine_id=options.machine_id, demo_networks=demo_networks)
763-
764- if not options.bundle:
765- runner = EC2TestRunner(
766- branch, email=email, file=options.file,
767- test_options=options.test_options, headless=options.headless,
768- branches=branches,
769- pqm_message=options.pqm_message,
770- pqm_public_location=options.pqm_public_location,
771- pqm_submit_location=options.pqm_submit_location,
772- open_browser=options.open_browser, pqm_email=options.pqm_email,
773- include_download_cache_changes=options.include_download_cache_changes,
774- instance=instance, vals=instance._vals,
775- )
776- def run_tests():
777- runner.configure_system()
778- runner.prepare_tests()
779- if demo_networks:
780- runner.start_demo_webserver()
781- else:
782- runner.run_tests()
783- return not options.headless
784- run = run_tests
785- else:
786- instance.check_bundling_prerequisites()
787- def make_new_image():
788- user_connection = instance.connect_as_user()
789- user_connection.perform('bzr launchpad-login %(launchpad-login)s')
790- if options.extra_update_image_commands:
791- for cmd in options.extra_update_image_commands:
792- user_connection.run_with_ssh_agent(cmd)
793- user_connection.run_with_ssh_agent(
794- "rsync -avp --partial --delete "
795- "--filter='P *.o' --filter='P *.pyc' --filter='P *.so' "
796- "devpad.canonical.com:/code/rocketfuel-built/launchpad/sourcecode/* "
797- "/var/launchpad/sourcecode/")
798- user_connection.run_with_ssh_agent(
799- 'bzr pull -d /var/launchpad/test ' + TRUNK_BRANCH)
800- user_connection.run_with_ssh_agent(
801- 'bzr pull -d /var/launchpad/download-cache lp:lp-source-dependencies')
802- user_connection.close()
803- root_connection = instance.connect_as_root()
804- root_connection.perform(
805- 'deluser --remove-home %(USER)s', ignore_failure=True)
806- root_connection.close()
807- instance.bundle(options.bundle, credentials)
808- return True
809- run = make_new_image
810-
811- instance.start()
812- instance.set_up_user(user_key)
813-
814- run_with_instance(
815- instance, run, options.demo_networks, options.postmortem)
816
817=== added file 'lib/devscripts/ec2test/controller.py'
818--- lib/devscripts/ec2test/controller.py 1970-01-01 00:00:00 +0000
819+++ lib/devscripts/ec2test/controller.py 2009-09-18 03:00:35 +0000
820@@ -0,0 +1,178 @@
821+# This file is incuded almost verbatim from commandant,
822+# https://launchpad.net/commandant. The only changes are removing some code
823+# we don't use that depends on other parts of commandant. When Launchpad is
824+# on Python 2.5 we can include commandant as an egg.
825+
826+
827+# Commandant is a framework for building command-oriented tools.
828+# Copyright (C) 2009 Jamshed Kakar.
829+#
830+# This program is free software; you can redistribute it and/or modify
831+# it under the terms of the GNU General Public License as published by
832+# the Free Software Foundation; either version 2 of the License, or
833+# (at your option) any later version.
834+#
835+# This program is distributed in the hope that it will be useful,
836+# but WITHOUT ANY WARRANTY; without even the implied warranty of
837+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
838+# GNU General Public License for more details.
839+#
840+# You should have received a copy of the GNU General Public License along
841+# with this program; if not, write to the Free Software Foundation, Inc.,
842+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
843+
844+"""Infrastructure to run C{bzrlib.commands.Command}s and L{HelpTopic}s."""
845+
846+import os
847+import sys
848+
849+from bzrlib.commands import run_bzr, Command
850+
851+
852+class CommandRegistry(object):
853+
854+ def __init__(self):
855+ self._commands = {}
856+
857+ def install_bzrlib_hooks(self):
858+ """
859+ Register this controller with C{Command.hooks} so that the controller
860+ can take advantage of Bazaar's command infrastructure.
861+
862+ L{_list_commands} and L{_get_command} are registered as callbacks for
863+ the C{list_commands} and C{get_commands} hooks, respectively.
864+ """
865+ Command.hooks.install_named_hook(
866+ "list_commands", self._list_commands, "commandant commands")
867+ Command.hooks.install_named_hook(
868+ "get_command", self._get_command, "commandant commands")
869+
870+ def _list_commands(self, names):
871+ """Hook to find C{bzrlib.commands.Command} names is called by C{bzrlib}.
872+
873+ @param names: A set of C{bzrlib.commands.Command} names to update with
874+ names from this controller.
875+ """
876+ names.update(self._commands.iterkeys())
877+ return names
878+
879+ def _get_command(self, command, name):
880+ """
881+ Hook to get the C{bzrlib.commands.Command} for C{name} is called by
882+ C{bzrlib}.
883+
884+ @param command: A C{bzrlib.commands.Command}, or C{None}, to be
885+ returned if a command matching C{name} can't be found.
886+ @param name: The name of the C{bzrlib.commands.Command} to retrieve.
887+ @return: The C{bzrlib.commands.Command} from the index or C{command}
888+ if one isn't available for C{name}.
889+ """
890+ try:
891+ local_command = self._commands[name]()
892+ except KeyError:
893+ for cmd in self._commands.itervalues():
894+ if name in cmd.aliases:
895+ local_command = cmd()
896+ break
897+ else:
898+ return command
899+ local_command.controller = self
900+ return local_command
901+
902+ def register_command(self, name, command_class):
903+ """Register a C{bzrlib.commands.Command} with this controller.
904+
905+ @param name: The name to register the command with.
906+ @param command_class: A type object, typically a subclass of
907+ C{bzrlib.commands.Command} to use when the command is invoked.
908+ """
909+ self._commands[name] = command_class
910+
911+ def load_module(self, module):
912+ """Load C{bzrlib.commands.Command}s and L{HelpTopic}s from C{module}.
913+
914+ Objects found in the module with names that start with C{cmd_} are
915+ treated as C{bzrlib.commands.Command}s and objects with names that
916+ start with C{topic_} are treated as L{HelpTopic}s.
917+ """
918+ for name in module.__dict__:
919+ if name.startswith("cmd_"):
920+ sanitized_name = name[4:].replace("_", "-")
921+ self.register_command(sanitized_name, module.__dict__[name])
922+ elif name.startswith("topic_"):
923+ sanitized_name = name[6:].replace("_", "-")
924+ self.register_help_topic(sanitized_name, module.__dict__[name])
925+
926+
927+class HelpTopicRegistry(object):
928+
929+ def __init__(self):
930+ self._help_topics = {}
931+
932+ def register_help_topic(self, name, help_topic_class):
933+ """Register a C{bzrlib.commands.Command} to this controller.
934+
935+ @param name: The name to register the command with.
936+ @param command_class: A type object, typically a subclass of
937+ C{bzrlib.commands.Command} to use when the command is invoked.
938+ """
939+ self._help_topics[name] = help_topic_class
940+
941+ def get_help_topic_names(self):
942+ """Get a C{set} of help topic names."""
943+ return set(self._help_topics.iterkeys())
944+
945+ def get_help_topic(self, name):
946+ """
947+ Get the help topic matching C{name} or C{None} if a match isn't found.
948+ """
949+ try:
950+ help_topic = self._help_topics[name]()
951+ except KeyError:
952+ return None
953+ help_topic.controller = self
954+ return help_topic
955+
956+
957+
958+class CommandExecutionMixin(object):
959+
960+ def run(self, argv):
961+ """Run the C{bzrlib.commands.Command} specified in C{argv}.
962+
963+ @raise BzrCommandError: Raised if a matching command can't be found.
964+ """
965+ run_bzr(argv)
966+
967+
968+
969+def import_module(filename, file_path, package_path):
970+ """Import a module and make it a child of C{commandant_command}.
971+
972+ The module source in C{filename} at C{file_path} is copied to a temporary
973+ directory, a Python package called C{commandant_command}.
974+
975+ @param filename: The name of the module file.
976+ @param file_path: The path to the module file.
977+ @param package_path: The path for the new C{commandant_command} package.
978+ @return: The new module.
979+ """
980+ module_path = os.path.join(package_path, "commandant_command")
981+ if not os.path.exists(module_path):
982+ os.mkdir(module_path)
983+
984+ init_path = os.path.join(module_path, "__init__.py")
985+ open(init_path, "w").close()
986+
987+ source_code = open(file_path, "r").read()
988+ module_file_path = os.path.join(module_path, filename)
989+ module_file = open(module_file_path, "w")
990+ module_file.write(source_code)
991+ module_file.close()
992+
993+ name = filename[:-3]
994+ sys.path.append(package_path)
995+ try:
996+ return __import__("commandant_command.%s" % (name,), fromlist=[name])
997+ finally:
998+ sys.path.pop()
999
1000=== modified file 'lib/devscripts/ec2test/credentials.py'
1001--- lib/devscripts/ec2test/credentials.py 2009-09-14 05:14:44 +0000
1002+++ lib/devscripts/ec2test/credentials.py 2009-09-18 01:33:42 +0000
1003@@ -13,9 +13,11 @@
1004
1005 import boto
1006
1007+from bzrlib.errors import BzrCommandError
1008+
1009 from devscripts.ec2test.account import EC2Account
1010
1011-class CredentialsError(Exception):
1012+class CredentialsError(BzrCommandError):
1013 """Raised when AWS credentials could not be loaded."""
1014
1015 def __init__(self, filename, extra=None):
1016
1017=== added file 'lib/devscripts/ec2test/entrypoint.py'
1018--- lib/devscripts/ec2test/entrypoint.py 1970-01-01 00:00:00 +0000
1019+++ lib/devscripts/ec2test/entrypoint.py 2009-09-18 03:10:25 +0000
1020@@ -0,0 +1,32 @@
1021+import readline
1022+import rlcompleter
1023+import sys
1024+
1025+from bzrlib.errors import BzrCommandError
1026+
1027+from devscripts.ec2test import builtins
1028+from devscripts.ec2test.controller import (
1029+ CommandRegistry, CommandExecutionMixin)
1030+
1031+# Shut up pyflakes.
1032+rlcompleter
1033+
1034+readline.parse_and_bind('tab: complete')
1035+
1036+class EC2CommandController(CommandRegistry, CommandExecutionMixin):
1037+ def __init__(self):
1038+ CommandRegistry.__init__(self)
1039+
1040+
1041+def main():
1042+ controller = EC2CommandController()
1043+ controller.install_bzrlib_hooks()
1044+ controller.load_module(builtins)
1045+
1046+ args = sys.argv[1:]
1047+ if not args:
1048+ args = ['help']
1049+ try:
1050+ controller.run(args)
1051+ except BzrCommandError, e:
1052+ sys.exit('ec2: ERROR: ' + str(e))
1053
1054=== modified file 'lib/devscripts/ec2test/instance.py'
1055--- lib/devscripts/ec2test/instance.py 2009-09-14 05:43:04 +0000
1056+++ lib/devscripts/ec2test/instance.py 2009-09-18 02:23:13 +0000
1057@@ -8,6 +8,7 @@
1058 'EC2Instance',
1059 ]
1060
1061+import code
1062 import glob
1063 import os
1064 import select
1065@@ -15,13 +16,15 @@
1066 import subprocess
1067 import sys
1068 import time
1069+import traceback
1070
1071+from bzrlib.errors import BzrCommandError
1072 from bzrlib.plugins.launchpad.account import get_lp_login
1073
1074 import paramiko
1075
1076-from devscripts.ec2test import error_and_quit
1077 from devscripts.ec2test.sshconfig import SSHConfig
1078+from devscripts.ec2test.credentials import EC2Credentials
1079
1080 DEFAULT_INSTANCE_TYPE = 'c1.xlarge'
1081 AVAILABLE_INSTANCE_TYPES = ('m1.large', 'm1.xlarge', 'c1.xlarge')
1082@@ -36,23 +39,45 @@
1083 pass
1084
1085
1086+def get_user_key():
1087+ """Get a SSH key from the agent. Exit if not found.
1088+
1089+ This key will be used to let the user log in (as $USER) to the instance.
1090+ """
1091+ agent = paramiko.Agent()
1092+ keys = agent.get_keys()
1093+ if len(keys) == 0:
1094+ raise BzrCommandError(
1095+ 'You must have an ssh agent running with keys installed that '
1096+ 'will allow the script to rsync to devpad and get your '
1097+ 'branch.\n')
1098+ user_key = agent.get_keys()[0]
1099+ return user_key
1100+
1101+
1102 class EC2Instance:
1103 """A single EC2 instance."""
1104
1105 @classmethod
1106- def make(cls, credentials, name, instance_type, machine_id, demo_networks):
1107+ def make(cls, name, instance_type, machine_id,
1108+ demo_networks=None, credentials=None):
1109 """Construct an `EC2Instance`.
1110
1111- :param credentials: An `EC2Credentials` object.
1112 :param name: The name to use for the key pair and security group for
1113 the instance.
1114 :param instance_type: One of the AVAILABLE_INSTANCE_TYPES.
1115- :param machine_id: ???
1116- :param demo_networks: ???
1117+ :param machine_id: The AMI to use, or None to do the usual regexp
1118+ matching.
1119+ :param demo_networks: The networks to add to the security group to
1120+ allow access to the instance.
1121+ :param credentials: An `EC2Credentials` object.
1122 """
1123 if instance_type not in AVAILABLE_INSTANCE_TYPES:
1124 raise ValueError('unknown instance_type %s' % (instance_type,))
1125
1126+ if credentials is None:
1127+ credentials = EC2Credentials.load_from_file()
1128+
1129 # Make the EC2 connection.
1130 account = credentials.connect(name)
1131
1132@@ -71,7 +96,7 @@
1133 vals = os.environ.copy()
1134 login = get_lp_login()
1135 if not login:
1136- error_and_quit(
1137+ raise BzrCommandError(
1138 'you must have set your launchpad login in bzr.')
1139 vals['launchpad-login'] = login
1140
1141@@ -104,7 +129,7 @@
1142 return
1143 start = time.time()
1144 self.private_key = self._account.acquire_private_key()
1145- self._account.acquire_security_group(
1146+ self.security_group = self._account.acquire_security_group(
1147 demo_networks=self._demo_networks)
1148 reservation = self._image.run(
1149 key_name=self._name, security_groups=[self._name],
1150@@ -123,7 +148,7 @@
1151 self._output = self._boto_instance.get_console_output()
1152 self.log(self._output.output)
1153 else:
1154- error_and_quit(
1155+ raise BzrCommandError(
1156 'failed to start: %s\n' % self._boto_instance.state)
1157
1158 def shutdown(self):
1159@@ -241,6 +266,50 @@
1160 as_root('rm -fr /var/tmp/*')
1161 root_connection.close()
1162
1163+ def set_up_and_run(self, postmortem, shutdown, func, *args, **kw):
1164+ """Start and set up, run `func` and then optionally shut down.
1165+
1166+ :param postmortem: If true, any exceptions will be caught and an
1167+ interactive session run to allow debugging the problem.
1168+ :param shutdown: If true, the instance will be shut down before this
1169+ function returns.
1170+ :param func: A callable that will be called when the instance is
1171+ running and a user account has been set up on it.
1172+ :param args: Passed to `func`.
1173+ :param kw: Passed to `func`.
1174+ """
1175+ user_key = get_user_key()
1176+ self.start()
1177+ try:
1178+ self.set_up_user(user_key)
1179+ try:
1180+ return func(*args, **kw)
1181+ except Exception:
1182+ # When running in postmortem mode, it is really helpful to see if
1183+ # there are any exceptions before it waits in the console (in the
1184+ # finally block), and you can't figure out why it's broken.
1185+ traceback.print_exc()
1186+ finally:
1187+ try:
1188+ if postmortem:
1189+ console = code.InteractiveConsole(locals())
1190+ console.interact((
1191+ 'Postmortem Console. EC2 instance is not yet dead.\n'
1192+ 'It will shut down when you exit this prompt (CTRL-D).\n'
1193+ '\n'
1194+ 'Tab-completion is enabled.'
1195+ '\n'
1196+ 'EC2Instance is available as `instance`.\n'
1197+ 'Also try these:\n'
1198+ ' http://%(dns)s/current_test.log\n'
1199+ ' ssh -A %(dns)s') %
1200+ {'dns': self.hostname})
1201+ print 'Postmortem console closed.'
1202+ finally:
1203+ if shutdown:
1204+ self.shutdown()
1205+
1206+
1207 def _copy_single_file(self, sftp, local_path, remote_dir):
1208 """Copy `local_path` to `remote_dir` on this instance.
1209
1210@@ -281,7 +350,7 @@
1211 pattern = os.path.join(local_dir, pattern)
1212 matches = glob.glob(pattern)
1213 if len(matches) != 1:
1214- error_and_quit(
1215+ raise BzrCommandError(
1216 '%r must match a single %s file' % (pattern, file_kind))
1217 return matches[0]
1218
1219@@ -290,12 +359,12 @@
1220 """
1221 local_ec2_dir = os.path.expanduser('~/.ec2')
1222 if not os.path.exists(local_ec2_dir):
1223- error_and_quit(
1224+ raise BzrCommandError(
1225 "~/.ec2 must exist and contain aws_user, aws_id, a private "
1226 "key file and a certificate.")
1227 aws_user_file = os.path.expanduser('~/.ec2/aws_user')
1228 if not os.path.exists(aws_user_file):
1229- error_and_quit(
1230+ raise BzrCommandError(
1231 "~/.ec2/aws_user must exist and contain your numeric AWS id.")
1232 self.aws_user = open(aws_user_file).read().strip()
1233 self.local_cert = self._check_single_glob_match(
1234
1235=== modified file 'lib/devscripts/ec2test/testrunner.py'
1236--- lib/devscripts/ec2test/testrunner.py 2009-09-15 00:14:56 +0000
1237+++ lib/devscripts/ec2test/testrunner.py 2009-09-18 02:23:13 +0000
1238@@ -14,9 +14,6 @@
1239 import re
1240 import sys
1241
1242-
1243-from bzrlib.plugin import load_plugins
1244-load_plugins()
1245 from bzrlib.branch import Branch
1246 from bzrlib.bzrdir import BzrDir
1247 from bzrlib.config import GlobalConfig
1248@@ -169,12 +166,6 @@
1249 self.headless = headless
1250 self.include_download_cache_changes = include_download_cache_changes
1251 self.open_browser = open_browser
1252- if headless and file:
1253- raise ValueError(
1254- 'currently do not support files with headless mode.')
1255- if headless and not (email or pqm_message):
1256- raise ValueError('You have specified no way to get the results '
1257- 'of your headless test run.')
1258
1259 if test_options != '-vv' and pqm_message is not None:
1260 raise ValueError(
1261@@ -375,12 +366,6 @@
1262 sys.stdout.write(msg)
1263 sys.stdout.flush()
1264
1265- def shutdown(self):
1266- if self.headless and self._running:
1267- self.log('letting instance run, to shut down headlessly '
1268- 'at completion of tests.\n')
1269- return
1270- return self._instance.shutdown()
1271
1272 def configure_system(self):
1273 user_connection = self._instance.connect_as_user()
1274@@ -487,10 +472,13 @@
1275 # close ssh connection
1276 user_connection.close()
1277
1278- def start_demo_webserver(self):
1279+ def run_demo_server(self):
1280 """Turn ec2 instance into a demo server."""
1281+ self.configure_system()
1282+ self.prepare_tests()
1283 user_connection = self._instance.connect_as_user()
1284 p = user_connection.perform
1285+ p('make -C /var/launchpad/test schema')
1286 p('mkdir -p /var/tmp/bazaar.launchpad.dev/static')
1287 p('mkdir -p /var/tmp/bazaar.launchpad.dev/mirrors')
1288 p('sudo a2enmod proxy > /dev/null')
1289@@ -520,6 +508,8 @@
1290 user_connection.close()
1291
1292 def run_tests(self):
1293+ self.configure_system()
1294+ self.prepare_tests()
1295 user_connection = self._instance.connect_as_user()
1296
1297 # Make sure we activate the failsafe --shutdown feature. This will
1298
1299=== added file 'utilities/ec2'
1300--- utilities/ec2 1970-01-01 00:00:00 +0000
1301+++ utilities/ec2 2009-09-16 01:34:30 +0000
1302@@ -0,0 +1,17 @@
1303+#!/usr/bin/python
1304+
1305+# Copyright 2009 Canonical Ltd. This software is licensed under the
1306+# GNU Affero General Public License version 3 (see the file LICENSE).
1307+
1308+"""Executable for the ec2 scripts."""
1309+
1310+__metaclass__ = type
1311+
1312+import os
1313+import sys
1314+
1315+sys.path.append(
1316+ os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib'))
1317+
1318+from devscripts.ec2test.entrypoint import main
1319+main()
1320
1321=== modified file 'utilities/ec2test.py'
1322--- utilities/ec2test.py 2009-09-11 22:49:43 +0000
1323+++ utilities/ec2test.py 2009-09-18 03:10:03 +0000
1324@@ -1,17 +1,10 @@
1325-#!/usr/bin/python
1326+#!/bin/sh
1327
1328 # Copyright 2009 Canonical Ltd. This software is licensed under the
1329 # GNU Affero General Public License version 3 (see the file LICENSE).
1330
1331-"""Executable for the ec2test script."""
1332-
1333-__metaclass__ = type
1334-
1335-import os
1336-import sys
1337-
1338-sys.path.append(
1339- os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib'))
1340-
1341-from devscripts.ec2test import main
1342-main()
1343+echo "You should run $(dirname $0)/ec2 test" $@ "instead." >/dev/null 1>&2
1344+echo "Waiting for 5 seconds in case you're not reading this." >/dev/null 1>&2
1345+sleep 5
1346+
1347+exec $(dirname $0)/ec2 test "$@"