Merge lp:~jml/launchpad/ec2-muck-around into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/ec2-muck-around
Merge into: lp:launchpad
Diff against target: 569 lines
3 files modified
lib/devscripts/ec2test/builtins.py (+67/-32)
lib/devscripts/ec2test/instance.py (+32/-27)
lib/devscripts/ec2test/testrunner.py (+32/-87)
To merge this branch: bzr merge lp:~jml/launchpad/ec2-muck-around
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+12844@code.launchpad.net

Commit message

Clean up the ec2 tools a bit, removing the vals dict and increasing PEP 8 compliance.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

I got bored today and started hacking on this branch. It doesn't really add any features or change any behaviour (I hope), but it cleans up the ec2 tools a little. Most of the work is focused around the EC2TestRunner constructor, which I was trying to understand.

Here's what I did specifically:

  * More PEP 8 compliance: 80 cols, whitespace etc.
  * Cleanup of spurious comments
  * Move some option checking to the builtins module and out of the constructor
  * Drop all uses of the 'vals' dict.

I haven't really tested it, since I'm not sure how.

jml

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hi,

> I got bored today and started hacking on this branch. It doesn't really add
> any features or change any behaviour (I hope), but it cleans up the ec2 tools
> a little. Most of the work is focused around the EC2TestRunner constructor,
> which I was trying to understand.
>
> Here's what I did specifically:
>
> * More PEP 8 compliance: 80 cols, whitespace etc.
> * Cleanup of spurious comments
> * Move some option checking to the builtins module and out of the
> constructor
> * Drop all uses of the 'vals' dict.

This all sounds great, particularly all the line lengths that I was too lazy to fix.

> I haven't really tested it, since I'm not sure how.

You just need to run all the commands I think :/ Landing it via ec2 test -s you can test one of them at least :-)

Cheers,
mwh

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/builtins.py'
2--- lib/devscripts/ec2test/builtins.py 2009-10-03 08:19:54 +0000
3+++ lib/devscripts/ec2test/builtins.py 2009-10-04 16:05:20 +0000
4@@ -108,6 +108,32 @@
5 'and/or of this script.'))
6
7
8+def filename_type(filename):
9+ """An option validator for filenames.
10+
11+ :raise: an error if 'filename' is not a file we can write to.
12+ :return: 'filename' otherwise.
13+ """
14+ if filename is None:
15+ return filename
16+
17+ check_file = filename
18+ if os.path.exists(check_file):
19+ if not os.path.isfile(check_file):
20+ raise BzrCommandError(
21+ 'file argument %s exists and is not a file' % (filename,))
22+ else:
23+ check_file = os.path.dirname(check_file)
24+ if (not os.path.exists(check_file) or
25+ not os.path.isdir(check_file)):
26+ raise BzrCommandError(
27+ 'file %s cannot be created.' % (filename,))
28+ if not os.access(check_file, os.W_OK):
29+ raise BzrCommandError(
30+ 'you do not have permission to write %s' % (filename,))
31+ return filename
32+
33+
34 class EC2Command(Command):
35 """Subclass of `Command` that customizes usage to say 'ec2' not 'bzr'.
36
37@@ -164,51 +190,54 @@
38 machine_id_option,
39 instance_type_option,
40 Option(
41- 'file', short_name='f',
42+ 'file', short_name='f', type=filename_type,
43 help=('Store abridged test results in FILE.')),
44 ListOption(
45 'email', short_name='e', argname='EMAIL', type=str,
46- help=('Email address to which results should be mailed. Defaults to '
47- 'the email address from `bzr whoami`. May be supplied multiple '
48- 'times. The first supplied email address will be used as the '
49- 'From: address.')),
50+ help=('Email address to which results should be mailed. '
51+ 'Defaults to the email address from `bzr whoami`. May be '
52+ 'supplied multiple times. The first supplied email address '
53+ 'will be used as the From: address.')),
54 Option(
55 'noemail', short_name='n',
56 help=('Do not try to email results.')),
57 Option(
58 'test-options', short_name='o', type=str,
59- help=('Test options to pass to the remote test runner. Defaults to '
60- "``-o '-vv'``. For instance, to run specific tests, you might "
61- "use ``-o '-vvt my_test_pattern'``.")),
62+ help=('Test options to pass to the remote test runner. Defaults '
63+ "to ``-o '-vv'``. For instance, to run specific tests, "
64+ "you might use ``-o '-vvt my_test_pattern'``.")),
65 Option(
66 'submit-pqm-message', short_name='s', type=str, argname="MSG",
67- help=('A pqm message to submit if the test run is successful. If '
68- 'provided, you will be asked for your GPG passphrase before '
69- 'the test run begins.')),
70+ help=(
71+ 'A pqm message to submit if the test run is successful. If '
72+ 'provided, you will be asked for your GPG passphrase before '
73+ 'the test run begins.')),
74 Option(
75 'pqm-public-location', type=str,
76- help=('The public location for the pqm submit, if a pqm message is '
77- 'provided (see --submit-pqm-message). If this is not provided, '
78- 'for local branches, bzr configuration is consulted; for '
79- 'remote branches, it is assumed that the remote branch *is* '
80- 'a public branch.')),
81+ help=('The public location for the pqm submit, if a pqm message '
82+ 'is provided (see --submit-pqm-message). If this is not '
83+ 'provided, for local branches, bzr configuration is '
84+ 'consulted; for remote branches, it is assumed that the '
85+ 'remote branch *is* a public branch.')),
86 Option(
87 'pqm-submit-location', type=str,
88- help=('The submit location for the pqm submit, if a pqm message is '
89- 'provided (see --submit-pqm-message). If this option is not '
90- 'provided, the script will look for an explicitly specified '
91- 'launchpad branch using the -b/--branch option; if that branch '
92- 'was specified and is owned by the launchpad-pqm user on '
93- 'launchpad, it is used as the pqm submit location. Otherwise, '
94- 'for local branches, bzr configuration is consulted; for '
95- 'remote branches, it is assumed that the submit branch is %s.'
96+ help=('The submit location for the pqm submit, if a pqm message '
97+ 'is provided (see --submit-pqm-message). If this option '
98+ 'is not provided, the script will look for an explicitly '
99+ 'specified launchpad branch using the -b/--branch option; '
100+ 'if that branch was specified and is owned by the '
101+ 'launchpad-pqm user on launchpad, it is used as the pqm '
102+ 'submit location. Otherwise, for local branches, bzr '
103+ 'configuration is consulted; for remote branches, it is '
104+ 'assumed that the submit branch is %s.'
105 % (TRUNK_BRANCH,))),
106 Option(
107 'pqm-email', type=str,
108- help=('Specify the email address of the PQM you are submitting to. '
109- 'If the branch is local, then the bzr configuration is '
110- 'consulted; for remote branches "Launchpad PQM '
111- '<launchpad@pqm.canonical.com>" is used by default.')),
112+ help=(
113+ 'Specify the email address of the PQM you are submitting to. '
114+ 'If the branch is local, then the bzr configuration is '
115+ 'consulted; for remote branches "Launchpad PQM '
116+ '<launchpad@pqm.canonical.com>" is used by default.')),
117 postmortem_option,
118 Option(
119 'headless',
120@@ -252,6 +281,10 @@
121 'You have specified no way to get the results '
122 'of your headless test run.')
123
124+ if test_options != '-vv' and submit_pqm_message is not None:
125+ raise BzrCommandError(
126+ "Submitting to PQM with non-default test options isn't "
127+ "supported")
128
129 instance = EC2Instance.make(
130 EC2TestRunner.name, instance_type, machine)
131@@ -264,7 +297,7 @@
132 pqm_submit_location=pqm_submit_location,
133 open_browser=open_browser, pqm_email=pqm_email,
134 include_download_cache_changes=include_download_cache_changes,
135- instance=instance, vals=instance._vals)
136+ instance=instance, launchpad_login=instance._launchpad_login)
137
138 instance.set_up_and_run(postmortem, not headless, runner.run_tests)
139
140@@ -387,7 +420,7 @@
141 runner = EC2TestRunner(
142 test_branch, branches=branches,
143 include_download_cache_changes=include_download_cache_changes,
144- instance=instance, vals=instance._vals)
145+ instance=instance)
146
147 demo_network_string = '\n'.join(
148 ' ' + network for network in demo)
149@@ -483,13 +516,15 @@
150 directory before bundling.
151 """
152 user_connection = instance.connect()
153- user_connection.perform('bzr launchpad-login %(launchpad-login)s')
154+ user_connection.perform(
155+ 'bzr launchpad-login %s' % (instance._launchpad_login,))
156 for cmd in extra_update_image_command:
157 user_connection.run_with_ssh_agent(cmd)
158 user_connection.run_with_ssh_agent(
159 'bzr pull -d /var/launchpad/test ' + TRUNK_BRANCH)
160 user_connection.run_with_ssh_agent(
161- 'bzr pull -d /var/launchpad/download-cache lp:lp-source-dependencies')
162+ 'bzr pull -d /var/launchpad/download-cache '
163+ 'lp:lp-source-dependencies')
164 if public:
165 update_sourcecode_options = '--public-only'
166 else:
167
168=== modified file 'lib/devscripts/ec2test/instance.py'
169--- lib/devscripts/ec2test/instance.py 2009-10-03 08:19:54 +0000
170+++ lib/devscripts/ec2test/instance.py 2009-10-04 16:05:20 +0000
171@@ -29,13 +29,14 @@
172 DEFAULT_INSTANCE_TYPE = 'c1.xlarge'
173 AVAILABLE_INSTANCE_TYPES = ('m1.large', 'm1.xlarge', 'c1.xlarge')
174
175+
176 class AcceptAllPolicy:
177 """We accept all unknown host key."""
178
179- # Normally the console output is supposed to contain the Host key
180- # but it doesn't seem to be the case here, so we trust that the host
181- # we are connecting to is the correct one.
182 def missing_host_key(self, client, hostname, key):
183+ # Normally the console output is supposed to contain the Host key but
184+ # it doesn't seem to be the case here, so we trust that the host we
185+ # are connecting to is the correct one.
186 pass
187
188
189@@ -210,28 +211,26 @@
190 # get the image
191 image = account.acquire_image(machine_id)
192
193- vals = os.environ.copy()
194 login = get_lp_login()
195 if not login:
196 raise BzrCommandError(
197 'you must have set your launchpad login in bzr.')
198- vals['launchpad-login'] = login
199
200 return EC2Instance(
201- name, image, instance_type, demo_networks, account, vals,
202- from_scratch, user_key)
203+ name, image, instance_type, demo_networks, account,
204+ from_scratch, user_key, login)
205
206 def __init__(self, name, image, instance_type, demo_networks, account,
207- vals, from_scratch, user_key):
208+ from_scratch, user_key, launchpad_login):
209 self._name = name
210 self._image = image
211 self._account = account
212 self._instance_type = instance_type
213 self._demo_networks = demo_networks
214 self._boto_instance = None
215- self._vals = vals
216 self._from_scratch = from_scratch
217 self._user_key = user_key
218+ self._launchpad_login = launchpad_login
219
220 def log(self, msg):
221 """Log a message on stdout, flushing afterwards."""
222@@ -320,7 +319,8 @@
223 """
224 authorized_keys_file = conn.sftp.open(remote_filename, 'w')
225 authorized_keys_file.write(
226- "%s %s\n" % (self._user_key.get_name(), self._user_key.get_base64()))
227+ "%s %s\n" % (
228+ self._user_key.get_name(), self._user_key.get_base64()))
229 authorized_keys_file.close()
230
231 def _ensure_ec2test_user_has_keys(self, connection=None):
232@@ -361,11 +361,13 @@
233 self._upload_local_key(root_connection, 'local_key')
234 root_connection.perform(
235 'cat local_key >> ~/.ssh/authorized_keys && rm local_key')
236- root_connection.run_script(from_scratch_root % self._vals)
237+ root_connection.run_script(from_scratch_root)
238 self._ensure_ec2test_user_has_keys(root_connection)
239 root_connection.close()
240 conn = self._connect('ec2test')
241- conn.run_script(from_scratch_ec2test % self._vals)
242+ conn.run_script(
243+ from_scratch_ec2test
244+ % {'launchpad-login': self._launchpad_login})
245 self._from_scratch = False
246 return conn
247 self._ensure_ec2test_user_has_keys()
248@@ -390,9 +392,10 @@
249 try:
250 return func(*args, **kw)
251 except Exception:
252- # When running in postmortem mode, it is really helpful to see if
253- # there are any exceptions before it waits in the console (in the
254- # finally block), and you can't figure out why it's broken.
255+ # When running in postmortem mode, it is really helpful to see
256+ # if there are any exceptions before it waits in the console
257+ # (in the finally block), and you can't figure out why it's
258+ # broken.
259 traceback.print_exc()
260 finally:
261 try:
262@@ -400,7 +403,8 @@
263 console = code.InteractiveConsole(locals())
264 console.interact((
265 'Postmortem Console. EC2 instance is not yet dead.\n'
266- 'It will shut down when you exit this prompt (CTRL-D).\n'
267+ 'It will shut down when you exit this prompt '
268+ '(CTRL-D).\n'
269 '\n'
270 'Tab-completion is enabled.'
271 '\n'
272@@ -486,7 +490,7 @@
273 connection.perform('sudo mkdir /mnt/ec2')
274 connection.perform('sudo chown $USER:$USER /mnt/ec2')
275
276- remote_pk, remote_cert = self.copy_key_and_certificate_to_image(
277+ remote_pk, remote_cert = self.copy_key_and_certificate_to_image(
278 connection.sftp)
279
280 bundle_dir = os.path.join('/mnt', name)
281@@ -530,7 +534,7 @@
282 'ec2-register',
283 '--private-key=%s' % self.local_pk,
284 '--cert=%s' % self.local_cert,
285- manifest_path
286+ manifest_path,
287 ]
288 self.log("Executing command: %s" % ' '.join(cmd))
289 subprocess.check_call(cmd, env=env)
290@@ -551,14 +555,18 @@
291 self._sftp = self._ssh.open_sftp()
292 return self._sftp
293
294- def perform(self, cmd, ignore_failure=False, out=None):
295+ def perform(self, cmd, ignore_failure=False, out=None, err=None):
296 """Perform 'cmd' on server.
297
298 :param ignore_failure: If False, raise an error on non-zero exit
299 statuses.
300 :param out: A stream to write the output of the remote command to.
301+ :param err: A stream to write the error of the remote command to.
302 """
303- cmd = cmd % self._instance._vals
304+ if out is None:
305+ out = sys.stdout
306+ if err is None:
307+ err = sys.stderr
308 self._instance.log(
309 '%s@%s$ %s\n'
310 % (self._username, self._instance._boto_instance.id, cmd))
311@@ -570,15 +578,13 @@
312 if session.recv_ready():
313 data = session.recv(4096)
314 if data:
315- sys.stdout.write(data)
316- sys.stdout.flush()
317- if out is not None:
318- out.write(data)
319+ out.write(data)
320+ out.flush()
321 if session.recv_stderr_ready():
322 data = session.recv_stderr(4096)
323 if data:
324- sys.stderr.write(data)
325- sys.stderr.flush()
326+ err.write(data)
327+ err.flush()
328 if session.exit_status_ready():
329 break
330 session.close()
331@@ -598,7 +604,6 @@
332 Use this to run commands that require local SSH credentials. For
333 example, getting private branches from Launchpad.
334 """
335- cmd = cmd % self._instance._vals
336 self._instance.log(
337 '%s@%s$ %s\n'
338 % (self._username, self._instance._boto_instance.id, cmd))
339
340=== modified file 'lib/devscripts/ec2test/testrunner.py'
341--- lib/devscripts/ec2test/testrunner.py 2009-09-27 20:32:54 +0000
342+++ lib/devscripts/ec2test/testrunner.py 2009-10-04 16:05:20 +0000
343@@ -33,26 +33,6 @@
344 self,
345 "Couldn't parse '%s', not a Launchpad branch." % (branch_url,))
346
347-def validate_file(filename):
348- """Raise an error if 'filename' is not a file we can write to."""
349- if filename is None:
350- return
351-
352- check_file = filename
353- if os.path.exists(check_file):
354- if not os.path.isfile(check_file):
355- raise ValueError(
356- 'file argument %s exists and is not a file' % (filename,))
357- else:
358- check_file = os.path.dirname(check_file)
359- if (not os.path.exists(check_file) or
360- not os.path.isdir(check_file)):
361- raise ValueError(
362- 'file %s cannot be created.' % (filename,))
363- if not os.access(check_file, os.W_OK):
364- raise ValueError(
365- 'you do not have permission to write %s' % (filename,))
366-
367
368 def parse_branch_url(branch_url):
369 """Given the URL of a branch, return its components in a dict."""
370@@ -131,11 +111,11 @@
371
372 def __init__(self, branch, email=False, file=None, test_options='-vv',
373 headless=False, branches=(),
374- machine_id=None,
375 pqm_message=None, pqm_public_location=None,
376 pqm_submit_location=None,
377 open_browser=False, pqm_email=None,
378- include_download_cache_changes=None, instance=None, vals=None):
379+ include_download_cache_changes=None, instance=None,
380+ launchpad_login=None):
381 """Create a new EC2TestRunner.
382
383 This sets the following attributes:
384@@ -148,29 +128,15 @@
385 - message (after validating PQM submisson)
386 - email (after validating email capabilities)
387 - image (after connecting to ec2)
388- - file (after checking we can write to it)
389- - vals, a dict containing
390- - the environment
391- - trunk_branch (either from global or derived from branches)
392- - branch
393- - smtp_server
394- - smtp_username
395- - smtp_password
396- - email (distinct from the email attribute)
397- - key_type
398- - key
399- - launchpad_login
400+ - file
401 """
402- self.original_branch = branch # just for easy access in debugging
403+ self.original_branch = branch
404 self.test_options = test_options
405 self.headless = headless
406 self.include_download_cache_changes = include_download_cache_changes
407 self.open_browser = open_browser
408-
409- if test_options != '-vv' and pqm_message is not None:
410- raise ValueError(
411- "Submitting to PQM with non-default test options isn't "
412- "supported")
413+ self.file = file
414+ self._launchpad_login = launchpad_login
415
416 trunk_specified = False
417 trunk_branch = TRUNK_BRANCH
418@@ -195,7 +161,7 @@
419 if user == 'launchpad-pqm':
420 trunk_specified = True
421 trunk_branch = src
422-
423+ self._trunk_branch = trunk_branch
424 self.branches = branches.items()
425
426 # XXX: JonathanLange 2009-05-31: The trunk_specified stuff above and
427@@ -296,6 +262,8 @@
428 '--ignore-download-cache-changes, -c and -g '
429 'respectively), or '
430 'commit or remove the files in the download-cache.')
431+ self._branch = branch
432+
433 if email is not False:
434 if email is True:
435 email = [config.username()]
436@@ -308,54 +276,32 @@
437 for item in email:
438 if not isinstance(item, basestring):
439 raise ValueError(
440- 'email must be True, False, a string, or a list of '
441- 'strings')
442+ 'email must be True, False, a string, or a list '
443+ 'of strings')
444 tmp.append(item)
445 email = tmp
446 else:
447 email = None
448 self.email = email
449
450- # We do a lot of looking before leaping here because we want to avoid
451- # wasting time and money on errors we could have caught early.
452-
453- # Validate and set file.
454- validate_file(file)
455- self.file = file
456-
457- # Make a dict for string substitution based on the environ.
458- #
459- # XXX: JonathanLange 2009-06-02: Although this defintely makes the
460- # scripts & commands easier to write, it makes it harder to figure out
461- # how the different bits of the system interoperate (passing 'vals' to
462- # a method means it uses...?). Consider changing things around so that
463- # vals is not needed.
464- self.vals = vals
465- self.vals['trunk_branch'] = trunk_branch
466- self.vals['branch'] = branch
467-
468 # Email configuration.
469 if email is not None or pqm_message is not None:
470- server = self.vals['smtp_server'] = config.get_user_option(
471- 'smtp_server')
472- if server is None or server == 'localhost':
473+ self._smtp_server = config.get_user_option('smtp_server')
474+ if self._smtp_server is None or self._smtp_server == 'localhost':
475 raise ValueError(
476 'To send email, a remotely accessible smtp_server (and '
477 'smtp_username and smtp_password, if necessary) must be '
478 'configured in bzr. See the SMTP server information '
479 'here: https://wiki.canonical.com/EmailSetup .')
480- self.vals['smtp_username'] = config.get_user_option(
481- 'smtp_username')
482- self.vals['smtp_password'] = config.get_user_option(
483- 'smtp_password')
484+ self._smtp_username = config.get_user_option('smtp_username')
485+ self._smtp_password = config.get_user_option('smtp_password')
486 from_email = config.username()
487 if not from_email:
488+ # XXX: JonathanLange 2009-10-04: Is this strictly true? I
489+ # can't actually see where this is used.
490 raise ValueError(
491 'To send email, your bzr email address must be set '
492 '(use ``bzr whoami``).')
493- else:
494- self.vals['email'] = (
495- from_email.encode('utf8').encode('string-escape'))
496
497 self._instance = instance
498
499@@ -366,7 +312,6 @@
500 sys.stdout.write(msg)
501 sys.stdout.flush()
502
503-
504 def configure_system(self):
505 user_connection = self._instance.connect()
506 as_user = user_connection.perform
507@@ -376,13 +321,13 @@
508 bazaar_conf_file = user_connection.sftp.open(
509 ".bazaar/bazaar.conf", 'w')
510 bazaar_conf_file.write(
511- 'smtp_server = %(smtp_server)s\n' % self.vals)
512- if self.vals['smtp_username']:
513- bazaar_conf_file.write(
514- 'smtp_username = %(smtp_username)s\n' % self.vals)
515- if self.vals['smtp_password']:
516- bazaar_conf_file.write(
517- 'smtp_password = %(smtp_password)s\n' % self.vals)
518+ 'smtp_server = %s\n' % (self._smtp_server,))
519+ if self._smtp_username:
520+ bazaar_conf_file.write(
521+ 'smtp_username = %s\n' % (self._smtp_username,))
522+ if self._smtp_password:
523+ bazaar_conf_file.write(
524+ 'smtp_password = %s\n' % (self._smtp_password,))
525 bazaar_conf_file.close()
526 # Copy remote ec2-remote over
527 self.log('Copying ec2test-remote.py to remote machine.\n')
528@@ -391,7 +336,7 @@
529 'ec2test-remote.py'),
530 '/var/launchpad/ec2test-remote.py')
531 # Set up launchpad login and email
532- as_user('bzr launchpad-login %(launchpad-login)s')
533+ as_user('bzr launchpad-login %s' % (self._launchpad_login,))
534 user_connection.close()
535
536 def prepare_tests(self):
537@@ -400,11 +345,11 @@
538 user_connection.perform('rm -rf /var/launchpad/test')
539 # Get trunk.
540 user_connection.run_with_ssh_agent(
541- 'bzr branch %(trunk_branch)s /var/launchpad/test')
542+ 'bzr branch %s /var/launchpad/test' % (self._trunk_branch,))
543 # Merge the branch in.
544- if self.vals['branch'] is not None:
545+ if self._branch is not None:
546 user_connection.run_with_ssh_agent(
547- 'cd /var/launchpad/test; bzr merge %(branch)s')
548+ 'cd /var/launchpad/test; bzr merge %s' % (self._branch,))
549 else:
550 self.log('(Testing trunk, so no branch merge.)')
551 # get newest sources
552@@ -530,14 +475,14 @@
553 cmd.append('--daemon')
554
555 # Which branch do we want to test?
556- if self.vals['branch'] is not None:
557- branch = self.vals['branch']
558+ if self._branch is not None:
559+ branch = self._branch
560 remote_branch = Branch.open(branch)
561 branch_revno = remote_branch.revno()
562 else:
563- branch = self.vals['trunk_branch']
564+ branch = self._trunk_branch
565 branch_revno = None
566- cmd.append('--public-branch=%s' % branch)
567+ cmd.append('--public-branch=%s' % branch)
568 if branch_revno is not None:
569 cmd.append('--public-branch-revno=%d' % branch_revno)
570