Merge lp:~abentley/launchpad/upgrade-all-2 into lp:launchpad

Proposed by Aaron Bentley on 2012-03-06
Status: Merged
Merged at revision: 14926
Proposed branch: lp:~abentley/launchpad/upgrade-all-2
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/upgrade-all
Diff against target: 6185 lines (+2/-6072)
20 files modified
lib/devscripts/autoland.py (+0/-351)
lib/devscripts/ec2test/__init__.py (+0/-27)
lib/devscripts/ec2test/account.py (+0/-230)
lib/devscripts/ec2test/builtins.py (+0/-861)
lib/devscripts/ec2test/controller.py (+0/-181)
lib/devscripts/ec2test/credentials.py (+0/-76)
lib/devscripts/ec2test/entrypoint.py (+0/-49)
lib/devscripts/ec2test/instance.py (+0/-742)
lib/devscripts/ec2test/remote.py (+0/-930)
lib/devscripts/ec2test/session.py (+0/-96)
lib/devscripts/ec2test/testrunner.py (+0/-548)
lib/devscripts/ec2test/tests/__init__.py (+0/-2)
lib/devscripts/ec2test/tests/remote_daemonization_test.py (+0/-53)
lib/devscripts/ec2test/tests/test_ec2instance.py (+0/-140)
lib/devscripts/ec2test/tests/test_remote.py (+0/-1084)
lib/devscripts/ec2test/tests/test_session.py (+0/-71)
lib/devscripts/ec2test/tests/test_utils.py (+0/-60)
lib/devscripts/ec2test/utils.py (+0/-55)
lib/devscripts/tests/test_autoland.py (+0/-515)
scripts/upgrade_all_branches.py (+2/-1)
To merge this branch: bzr merge lp:~abentley/launchpad/upgrade-all-2
Reviewer Review Type Date Requested Status
Deryck Hodge (community) 2012-03-06 Approve on 2012-03-06
Review via email: mp+96247@code.launchpad.net

Commit Message

Remove ec2 scripts from launchpad tree

Description of the Change

= Summary =
ec2 scripts are not proper Launchpad scripts because they depend on pqm-submit, which Launchpad does not provide.

== Proposed fix ==
Add ec2 test/land scripts to lp-dev-tools and remove from Launchpad tree

== Pre-implementation notes ==
Discussed with deryck

== Implementation details ==
None

== Tests ==
None

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/bzr.py
  lib/lp/codehosting/tests/test_upgrade.py
  lib/lp/codehosting/vfs/branchfs.py
  scripts/upgrade_all_branches.py
  lib/lp/codehosting/bzrutils.py
  lib/lp/codehosting/scripts/tests/test_upgrade_all_branches.py
  lib/lp/codehosting/upgrade.py
  lib/lp/testing/__init__.py
  lib/lp/codehosting/vfs/tests/test_branchfs.py
  lib/lp_sitecustomize.py
  lib/lp/services/config/__init__.py

To post a comment you must log in.
Deryck Hodge (deryck) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'lib/devscripts/autoland.py'
2--- lib/devscripts/autoland.py 2012-02-09 13:08:47 +0000
3+++ lib/devscripts/autoland.py 1970-01-01 00:00:00 +0000
4@@ -1,351 +0,0 @@
5-"""Land an approved merge proposal."""
6-
7-from bzrlib.errors import BzrCommandError
8-from launchpadlib.launchpad import Launchpad
9-from launchpadlib.uris import (
10- DEV_SERVICE_ROOT,
11- EDGE_SERVICE_ROOT,
12- LPNET_SERVICE_ROOT,
13- STAGING_SERVICE_ROOT,
14- )
15-from lazr.uri import URI
16-
17-
18-class MissingReviewError(Exception):
19- """Raised when we try to get a review message without enough reviewers."""
20-
21-
22-class MissingBugsError(Exception):
23- """Merge proposal has no linked bugs and no [no-qa] tag."""
24-
25-
26-class MissingBugsIncrementalError(Exception):
27- """Merge proposal has the [incr] tag but no linked bugs."""
28-
29-
30-class LaunchpadBranchLander:
31-
32- name = 'launchpad-branch-lander'
33-
34- def __init__(self, launchpad):
35- self._launchpad = launchpad
36-
37- @classmethod
38- def load(cls, service_root='production'):
39- # XXX: JonathanLange 2009-09-24: No unit tests.
40- # XXX: JonathanLange 2009-09-24 bug=435813: If cached data invalid,
41- # there's no easy way to delete it and try again.
42- launchpad = Launchpad.login_with(cls.name, service_root)
43- return cls(launchpad)
44-
45- def load_merge_proposal(self, mp_url):
46- """Get the merge proposal object for the 'mp_url'."""
47- # XXX: JonathanLange 2009-09-24: No unit tests.
48- web_mp_uri = URI(mp_url)
49- api_mp_uri = self._launchpad._root_uri.append(
50- web_mp_uri.path.lstrip('/'))
51- return MergeProposal(self._launchpad.load(str(api_mp_uri)))
52-
53- def get_lp_branch(self, branch):
54- """Get the launchpadlib branch based on a bzr branch."""
55- # First try the public branch.
56- branch_url = branch.get_public_branch()
57- if branch_url:
58- lp_branch = self._launchpad.branches.getByUrl(
59- url=branch_url)
60- if lp_branch is not None:
61- return lp_branch
62- # If that didn't work try the push location.
63- branch_url = branch.get_push_location()
64- if branch_url:
65- lp_branch = self._launchpad.branches.getByUrl(
66- url=branch_url)
67- if lp_branch is not None:
68- return lp_branch
69- raise BzrCommandError(
70- "No public branch could be found. Please re-run and specify "
71- "the URL for the merge proposal.")
72-
73- def get_merge_proposal_from_branch(self, branch):
74- """Get the merge proposal from the branch."""
75-
76- lp_branch = self.get_lp_branch(branch)
77- proposals = [
78- mp for mp in lp_branch.landing_targets
79- if mp.queue_status in ('Needs review', 'Approved')]
80- if len(proposals) == 0:
81- raise BzrCommandError(
82- "The public branch has no open source merge proposals. "
83- "You must have a merge proposal before attempting to "
84- "land the branch.")
85- elif len(proposals) > 1:
86- raise BzrCommandError(
87- "The public branch has multiple open source merge "
88- "proposals. You must provide the URL to the one you wish "
89- "to use.")
90- return MergeProposal(proposals[0])
91-
92-
93-class MergeProposal:
94- """Wrapper around launchpadlib `IBranchMergeProposal` for landing."""
95-
96- def __init__(self, mp):
97- """Construct a merge proposal.
98-
99- :param mp: A launchpadlib `IBranchMergeProposal`.
100- """
101- self._mp = mp
102- self._launchpad = mp._root
103-
104- @property
105- def source_branch(self):
106- """The push URL of the source branch."""
107- return str(self._get_push_url(self._mp.source_branch))
108-
109- @property
110- def target_branch(self):
111- """The push URL of the target branch."""
112- return str(self._get_push_url(self._mp.target_branch))
113-
114- @property
115- def commit_message(self):
116- """The commit message specified on the merge proposal."""
117- return self._mp.commit_message
118-
119- @property
120- def is_approved(self):
121- """Is this merge proposal approved for landing."""
122- return self._mp.queue_status == 'Approved'
123-
124- def get_stakeholder_emails(self):
125- """Return a collection of people who should know about branch landing.
126-
127- Used to determine who to email with the ec2 test results.
128-
129- :return: A set of `IPerson`s.
130- """
131- # XXX: JonathanLange 2009-09-24: No unit tests.
132- emails = set(
133- map(get_email,
134- [self._mp.source_branch.owner, self._launchpad.me]))
135- if None in emails:
136- emails.remove(None)
137- return emails
138-
139- def get_reviews(self):
140- """Return a dictionary of all Approved reviews.
141-
142- Used to determine who has actually approved a branch for landing. The
143- key of the dictionary is the type of review, and the value is the list
144- of people who have voted Approve with that type.
145-
146- Common types include 'code', 'db', 'ui' and of course `None`.
147- """
148- reviews = {}
149- for vote in self._mp.votes:
150- comment = vote.comment
151- if comment is None or comment.vote != "Approve":
152- continue
153- reviewers = reviews.setdefault(vote.review_type, [])
154- reviewers.append(vote.reviewer)
155- if self.is_approved and not reviews:
156- reviews[None] = [self._mp.reviewer]
157- return reviews
158-
159- def get_bugs(self):
160- """Return a collection of bugs linked to the source branch."""
161- return self._mp.source_branch.linked_bugs
162-
163- def _get_push_url(self, branch):
164- """Return the push URL for 'branch'.
165-
166- This function is a work-around for Launchpad's lack of exposing the
167- branch's push URL.
168-
169- :param branch: A launchpadlib `IBranch`.
170- """
171- # XXX: JonathanLange 2009-09-24: No unit tests.
172- host = get_bazaar_host(str(self._launchpad._root_uri))
173- # XXX: JonathanLange 2009-09-24 bug=435790: lazr.uri allows a path
174- # without a leading '/' and then doesn't insert a '/' in the final
175- # URL. Do it ourselves.
176- return URI(scheme='bzr+ssh', host=host, path='/' + branch.unique_name)
177-
178- def build_commit_message(self, commit_text, testfix=False, no_qa=False,
179- incremental=False, rollback=None):
180- """Get the Launchpad-style commit message for a merge proposal."""
181- reviews = self.get_reviews()
182- bugs = self.get_bugs()
183-
184- tags = [
185- get_testfix_clause(testfix),
186- get_reviewer_clause(reviews),
187- get_bugs_clause(bugs),
188- get_qa_clause(bugs, no_qa,
189- incremental, rollback=rollback),
190- ]
191-
192- # Make sure we don't add duplicated tags to commit_text.
193- commit_tags = tags[:]
194- for tag in tags:
195- if tag in commit_text:
196- commit_tags.remove(tag)
197-
198- if commit_tags:
199- return '%s %s' % (''.join(commit_tags), commit_text)
200- else:
201- return commit_text
202-
203- def set_commit_message(self, commit_message):
204- """Set the Launchpad-style commit message for a merge proposal."""
205- self._mp.commit_message = commit_message
206- self._mp.lp_save()
207-
208-
209-def get_testfix_clause(testfix=False):
210- """Get the testfix clause."""
211- if testfix:
212- testfix_clause = '[testfix]'
213- else:
214- testfix_clause = ''
215- return testfix_clause
216-
217-
218-def get_qa_clause(bugs, no_qa=False, incremental=False, rollback=None):
219- """Check the no-qa and incremental options, getting the qa clause.
220-
221- The qa clause will always be or no-qa, or incremental, or no-qa and
222- incremental, or a revno for the rollback clause, or no tags.
223-
224- See https://dev.launchpad.net/QAProcessContinuousRollouts for detailed
225- explanation of each clause.
226- """
227- qa_clause = ""
228-
229- if not bugs and not no_qa and not incremental and not rollback:
230- raise MissingBugsError
231-
232- if incremental and not bugs:
233- raise MissingBugsIncrementalError
234-
235- if no_qa and incremental:
236- qa_clause = '[no-qa][incr]'
237- elif incremental:
238- qa_clause = '[incr]'
239- elif no_qa:
240- qa_clause = '[no-qa]'
241- elif rollback:
242- qa_clause = '[rollback=%d]' % rollback
243- else:
244- qa_clause = ''
245-
246- return qa_clause
247-
248-
249-def get_email(person):
250- """Get the preferred email address for 'person'."""
251- email_object = person.preferred_email_address
252- if email_object is None:
253- return None # A team most likely.
254- return email_object.email
255-
256-
257-def get_bugs_clause(bugs):
258- """Return the bugs clause of a commit message.
259-
260- :param bugs: A collection of `IBug` objects.
261- :return: A string of the form "[bug=A,B,C]".
262- """
263- if not bugs:
264- return ''
265- bug_ids = []
266- for bug in bugs:
267- for task in bug.bug_tasks:
268- if (task.bug_target_name == 'launchpad'
269- and task.status not in ['Fix Committed', 'Fix Released']):
270- bug_ids.append(str(bug.id))
271- break
272- if not bug_ids:
273- return ''
274- return '[bug=%s]' % ','.join(bug_ids)
275-
276-
277-def get_reviewer_handle(reviewer):
278- """Get the handle for 'reviewer'.
279-
280- The handles of reviewers are included in the commit message for Launchpad
281- changes. Historically, these handles have been the IRC nicks. Thus, if
282- 'reviewer' has an IRC nickname for Freenode, we use that. Otherwise we use
283- their Launchpad username.
284-
285- :param reviewer: A launchpadlib `IPerson` object.
286- :return: unicode text.
287- """
288- irc_handles = reviewer.irc_nicknames
289- for handle in irc_handles:
290- if handle.network == 'irc.freenode.net':
291- return handle.nickname
292- return reviewer.name
293-
294-
295-def _comma_separated_names(things):
296- """Return a string of comma-separated names of 'things'.
297-
298- The list is sorted before being joined.
299- """
300- return ','.join(sorted(thing.name for thing in things))
301-
302-
303-def get_reviewer_clause(reviewers):
304- """Get the reviewer section of a commit message, given the reviewers.
305-
306- :param reviewers: A dict mapping review types to lists of reviewers, as
307- returned by 'get_reviews'.
308- :return: A string like u'[r=foo,bar][ui=plop]'.
309- """
310- # If no review type is specified it is assumed to be a code review.
311- code_reviewers = reviewers.get(None, [])
312- ui_reviewers = []
313- rc_reviewers = []
314- for review_type, reviewer in reviewers.items():
315- if review_type is None:
316- continue
317- if review_type == '':
318- code_reviewers.extend(reviewer)
319- if 'code' in review_type or 'db' in review_type:
320- code_reviewers.extend(reviewer)
321- if 'ui' in review_type:
322- ui_reviewers.extend(reviewer)
323- if 'release-critical' in review_type:
324- rc_reviewers.extend(reviewer)
325- if not code_reviewers:
326- raise MissingReviewError("Need approved votes in order to land.")
327- if ui_reviewers:
328- ui_clause = '[ui=%s]' % _comma_separated_names(ui_reviewers)
329- else:
330- ui_clause = ''
331- if rc_reviewers:
332- rc_clause = (
333- '[release-critical=%s]' % _comma_separated_names(rc_reviewers))
334- else:
335- rc_clause = ''
336- return '%s[r=%s]%s' % (
337- rc_clause, _comma_separated_names(code_reviewers), ui_clause)
338-
339-
340-def get_bazaar_host(api_root):
341- """Get the Bazaar service for the given API root."""
342- # XXX: JonathanLange 2009-09-24 bug=435803: This is only needed because
343- # Launchpad doesn't expose the push URL for branches.
344- if api_root.startswith(EDGE_SERVICE_ROOT):
345- return 'bazaar.launchpad.net'
346- elif api_root.startswith(DEV_SERVICE_ROOT):
347- return 'bazaar.launchpad.dev'
348- elif api_root.startswith(STAGING_SERVICE_ROOT):
349- return 'bazaar.staging.launchpad.net'
350- elif api_root.startswith(LPNET_SERVICE_ROOT):
351- return 'bazaar.launchpad.net'
352- else:
353- raise ValueError(
354- 'Cannot determine Bazaar host. "%s" not a recognized Launchpad '
355- 'API root.' % (api_root,))
356
357=== removed directory 'lib/devscripts/ec2test'
358=== removed file 'lib/devscripts/ec2test/__init__.py'
359--- lib/devscripts/ec2test/__init__.py 2012-01-01 03:03:28 +0000
360+++ lib/devscripts/ec2test/__init__.py 1970-01-01 00:00:00 +0000
361@@ -1,27 +0,0 @@
362-# Copyright 2009 Canonical Ltd. This software is licensed under the
363-# GNU Affero General Public License version 3 (see the file LICENSE).
364-
365-"""Run the Launchpad tests in Amazon's Elastic Compute Cloud (EC2)."""
366-
367-__metaclass__ = type
368-
369-__all__ = []
370-
371-from bzrlib.plugin import load_plugins
372-
373-
374-load_plugins()
375-import paramiko
376-
377-#############################################################################
378-# Try to guide users past support problems we've encountered before
379-if not paramiko.__version__.startswith('1.7.4'):
380- raise RuntimeError('Your version of paramiko (%s) is not supported. '
381- 'Please use 1.7.4.' % (paramiko.__version__,))
382-# maybe add similar check for bzrlib?
383-# End
384-#############################################################################
385-
386-import warnings
387-warnings.filterwarnings(
388- "ignore", category=DeprecationWarning, module="boto")
389
390=== removed file 'lib/devscripts/ec2test/account.py'
391--- lib/devscripts/ec2test/account.py 2012-01-25 15:27:01 +0000
392+++ lib/devscripts/ec2test/account.py 1970-01-01 00:00:00 +0000
393@@ -1,230 +0,0 @@
394-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
395-# GNU Affero General Public License version 3 (see the file LICENSE).
396-
397-"""A representation of an Amazon Web Services account."""
398-
399-__metaclass__ = type
400-__all__ = [
401- 'EC2Account',
402- 'VALID_AMI_OWNERS',
403- ]
404-
405-from collections import defaultdict
406-import cStringIO
407-from datetime import datetime
408-from operator import itemgetter
409-import re
410-import sys
411-import urllib
412-
413-from boto.exception import EC2ResponseError
414-import paramiko
415-
416-from devscripts.ec2test.session import EC2SessionName
417-
418-
419-VALID_AMI_OWNERS = {
420- # Amazon account number: name/nickname (only for logging).
421- '255383312499': 'gary',
422- '559320013529': 'flacoste',
423- '038531743404': 'jelmer',
424- '444667466231': 'allenap',
425- '441991801793': 'gmb',
426- '005470753809': 'bigjools',
427- '967591634984': 'jtv',
428- '507541322704': 'sinzui',
429- '424228475252': 'wallyworld',
430- '292290876294': 'stevenk',
431- '259696152397': 'bac',
432- '873925794399': 'wgrant',
433- '957911449157': 'mbp',
434- '340983519589': 'stub',
435- # ...anyone else want in on the fun?
436- }
437-
438-AUTH_FAILURE_MESSAGE = """\
439-POSSIBLE CAUSES OF ERROR:
440-- Did you sign up for EC2?
441-- Did you put a credit card number in your AWS account?
442-Please double-check before reporting a problem.
443-"""
444-
445-
446-def get_ip():
447- """Uses AWS checkip to obtain this machine's IP address.
448-
449- Consults an external website to determine the public IP address of this
450- machine.
451-
452- :return: This machine's net-visible IP address as a string.
453- """
454- return urllib.urlopen('http://checkip.amazonaws.com').read().strip()
455-
456-
457-class EC2Account:
458- """An EC2 account.
459-
460- You can use this to manage security groups, keys and images for an EC2
461- account.
462- """
463-
464- # Used to find pre-configured Amazon images.
465- _image_match = re.compile(
466- r'launchpad-ec2test(\d+)/image.manifest.xml$').match
467-
468- def __init__(self, name, connection):
469- """Construct an EC2 instance.
470-
471- :param name: ???
472- :param connection: An open boto ec2 connection.
473- """
474- self.name = name
475- self.conn = connection
476-
477- def log(self, msg):
478- """Log a message on stdout, flushing afterwards."""
479- # XXX: JonathanLange 2009-05-31 bug=383076: Copied from EC2TestRunner.
480- # Should change EC2Account to take a logger and use that instead of
481- # writing to stdout.
482- sys.stdout.write(msg)
483- sys.stdout.flush()
484-
485- def _find_expired_artifacts(self, artifacts):
486- now = datetime.utcnow()
487- for artifact in artifacts:
488- session_name = EC2SessionName(artifact.name)
489- if (session_name in (self.name, self.name.base) or (
490- session_name.base == self.name.base and
491- session_name.expires is not None and
492- session_name.expires < now)):
493- yield artifact
494-
495- def acquire_security_group(self, demo_networks=None):
496- """Get a security group with the appropriate configuration.
497-
498- "Appropriate" means configured to allow this machine to connect via
499- SSH, HTTP and HTTPS.
500-
501- The name of the security group is the `EC2Account.name` attribute.
502-
503- :return: A boto security group.
504- """
505- if demo_networks is None:
506- demo_networks = []
507- # Create the security group.
508- security_group = self.conn.create_security_group(
509- self.name, 'Authorization to access the test runner instance.')
510- # Authorize SSH and HTTP.
511- ip = get_ip()
512- security_group.authorize('tcp', 22, 22, '%s/32' % ip)
513- security_group.authorize('tcp', 80, 80, '%s/32' % ip)
514- security_group.authorize('tcp', 443, 443, '%s/32' % ip)
515- for network in demo_networks:
516- # Add missing netmask info for single ips.
517- if '/' not in network:
518- network += '/32'
519- security_group.authorize('tcp', 80, 80, network)
520- security_group.authorize('tcp', 443, 443, network)
521- return security_group
522-
523- def delete_previous_security_groups(self):
524- """Delete previously used security groups, if found."""
525- expired_groups = self._find_expired_artifacts(
526- self.conn.get_all_security_groups())
527- for group in expired_groups:
528- try:
529- group.delete()
530- except EC2ResponseError, e:
531- if e.code != 'InvalidGroup.InUse':
532- raise
533- self.log('Cannot delete; security group '
534- '%r in use.\n' % group.name)
535- else:
536- self.log('Deleted security group %r.\n' % group.name)
537-
538- def acquire_private_key(self):
539- """Create & return a new key pair for the test runner."""
540- key_pair = self.conn.create_key_pair(self.name)
541- return paramiko.RSAKey.from_private_key(
542- cStringIO.StringIO(key_pair.material.encode('ascii')))
543-
544- def delete_previous_key_pairs(self):
545- """Delete previously used keypairs, if found."""
546- expired_key_pairs = self._find_expired_artifacts(
547- self.conn.get_all_key_pairs())
548- for key_pair in expired_key_pairs:
549- try:
550- key_pair.delete()
551- except EC2ResponseError, e:
552- if e.code != 'InvalidKeyPair.NotFound':
553- if e.code == 'AuthFailure':
554- # Inserted because of previous support issue.
555- self.log(AUTH_FAILURE_MESSAGE)
556- raise
557- self.log('Cannot delete; key pair not '
558- 'found %r\n' % key_pair.name)
559- else:
560- self.log('Deleted key pair %r.\n' % key_pair.name)
561-
562- def collect_garbage(self):
563- """Remove any old keys and security groups."""
564- self.delete_previous_security_groups()
565- self.delete_previous_key_pairs()
566-
567- def find_images(self):
568- # We are trying to find an image that has a location that matches a
569- # regex (see definition of _image_match, above). Part of that regex is
570- # expected to be an integer with the semantics of a revision number.
571- # The image location with the highest revision number is the one that
572- # should be chosen. Because AWS does not guarantee that two images
573- # cannot share a location string, we need to make sure that the search
574- # result for this image is unique, or throw an error because the
575- # choice of image is ambiguous.
576- results = defaultdict(list)
577-
578- # Find the images with the highest revision numbers and locations that
579- # match the regex.
580- images = self.conn.get_all_images(owners=tuple(VALID_AMI_OWNERS))
581- for image in images:
582- match = self._image_match(image.location)
583- if match is not None:
584- revision = int(match.group(1))
585- results[revision].append(image)
586-
587- return sorted(results.iteritems(), key=itemgetter(0), reverse=True)
588-
589- def acquire_image(self, machine_id):
590- """Get the image.
591-
592- If 'machine_id' is None, then return the image with location that
593- matches `EC2Account._image_match` and has the highest revision number
594- (where revision number is the 'NN' in 'launchpad-ec2testNN').
595-
596- Otherwise, just return the image with the given 'machine_id'.
597-
598- :raise ValueError: if there is more than one image with the same
599- location string.
600-
601- :raise RuntimeError: if we cannot find a test-runner image.
602-
603- :return: A boto image.
604- """
605- if machine_id is not None:
606- # This may raise an exception. The user specified a machine_id, so
607- # they can deal with it.
608- return self.conn.get_image(machine_id)
609-
610- images_by_revision = self.find_images()
611- if len(images_by_revision) == 0:
612- raise RuntimeError(
613- "You don't have access to a test-runner image.\n"
614- "Request access and try again.\n")
615-
616- revision, images = images_by_revision[0]
617- if len(images) > 1:
618- raise ValueError(
619- 'More than one image of revision %d found: %r' % (
620- revision, images))
621-
622- self.log('Using machine image version %d\n' % revision)
623- return images[0]
624
625=== removed file 'lib/devscripts/ec2test/builtins.py'
626--- lib/devscripts/ec2test/builtins.py 2012-01-01 03:03:28 +0000
627+++ lib/devscripts/ec2test/builtins.py 1970-01-01 00:00:00 +0000
628@@ -1,861 +0,0 @@
629-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
630-# GNU Affero General Public License version 3 (see the file LICENSE).
631-
632-"""The command classes for the 'ec2' utility."""
633-
634-__metaclass__ = type
635-__all__ = []
636-
637-from datetime import (
638- datetime,
639- timedelta,
640- )
641-import os
642-import pdb
643-import socket
644-
645-from bzrlib.bzrdir import BzrDir
646-from bzrlib.commands import Command
647-from bzrlib.errors import (
648- BzrCommandError,
649- ConnectionError,
650- NoSuchFile,
651- )
652-from bzrlib.help import help_commands
653-from bzrlib.option import (
654- ListOption,
655- Option,
656- )
657-from bzrlib.trace import is_verbose
658-from bzrlib.transport import get_transport
659-from pytz import UTC
660-import simplejson
661-
662-from devscripts import get_launchpad_root
663-from devscripts.ec2test.account import VALID_AMI_OWNERS
664-from devscripts.ec2test.credentials import EC2Credentials
665-from devscripts.ec2test.instance import (
666- AVAILABLE_INSTANCE_TYPES,
667- DEFAULT_INSTANCE_TYPE,
668- DEFAULT_REGION,
669- EC2Instance,
670- )
671-from devscripts.ec2test.session import EC2SessionName
672-from devscripts.ec2test.testrunner import (
673- EC2TestRunner,
674- TRUNK_BRANCH,
675- )
676-
677-# Options accepted by more than one command.
678-
679-# Branches is a complicated option that lets the user specify which branches
680-# to use in the sourcecode directory. Most of the complexity is still in
681-# EC2TestRunner.__init__, which probably isn't ideal.
682-branch_option = ListOption(
683- 'branch', type=str, short_name='b', argname='BRANCH',
684- help=('Branches to include in this run in sourcecode. '
685- 'If the argument is only the project name, the trunk will be '
686- 'used (e.g., ``-b launchpadlib``). If you want to use a '
687- 'specific branch, if it is on launchpad, you can usually '
688- 'simply specify it instead (e.g., '
689- '``-b lp:~username/launchpadlib/branchname``). If this does '
690- 'not appear to work, or if the desired branch is not on '
691- 'launchpad, specify the project name and then the branch '
692- 'after an equals sign (e.g., '
693- '``-b launchpadlib=lp:~username/launchpadlib/branchname``). '
694- 'Branches for multiple projects may be specified with '
695- 'multiple instances of this option. '
696- 'You may also use this option to specify the branch of launchpad '
697- 'into which your branch may be merged. This defaults to %s. '
698- 'Because typically the important branches of launchpad are owned '
699- 'by the launchpad-pqm user, you can shorten this to only the '
700- 'branch name, if desired, and the launchpad-pqm user will be '
701- 'assumed. For instance, if you specify '
702- '``-b launchpad=db-devel`` then this is equivalent to '
703- '``-b lp:~launchpad-pqm/launchpad/db-devel``, or the even longer'
704- '``-b launchpad=lp:~launchpad-pqm/launchpad/db-devel``.'
705- % (TRUNK_BRANCH,)))
706-
707-
708-machine_id_option = Option(
709- 'machine', short_name='m', type=str,
710- help=('The AWS machine identifier (AMI) on which to base this run. '
711- 'You should typically only have to supply this if you are '
712- 'testing new AWS images. Defaults to trying to find the most '
713- 'recent one with an approved owner.'))
714-
715-
716-instance_type_option = Option(
717- 'instance', short_name='i',
718- type=str,
719- param_name='instance_type',
720- help=('The AWS instance type on which to base this run. '
721- 'Available options are %r. Defaults to `%s`.' %
722- (AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE)))
723-
724-
725-debug_option = Option(
726- 'debug', short_name='d',
727- help=('Drop to pdb trace as soon as possible.'))
728-
729-
730-trunk_option = Option(
731- 'trunk', short_name='t',
732- help=('Run the trunk as the branch, rather than the branch of the '
733- 'current working directory.'))
734-
735-
736-include_download_cache_changes_option = Option(
737- 'include-download-cache-changes', short_name='c',
738- help=('Include any changes in the download cache (added or unknown) '
739- 'in the download cache of the test run. Note that, if you have '
740- 'any changes in your download cache, trying to submit to pqm '
741- 'will always raise an error. Also note that, if you have any '
742- 'changes in your download cache, you must explicitly choose to '
743- 'include or ignore the changes.'))
744-
745-
746-postmortem_option = Option(
747- 'postmortem', short_name='p',
748- help=('Drop to interactive prompt after the test and before shutting '
749- 'down the instance for postmortem analysis of the EC2 instance '
750- 'and/or of this script.'))
751-
752-
753-attached_option = Option(
754- 'attached',
755- help=("Remain attached, i.e. do not go headless. Implied by --postmortem "
756- "and --file."))
757-
758-
759-region_option = Option(
760- 'region',
761- type=str,
762- help=("Name of the AWS region in which to run the instance. "
763- "Must be the same as the region holding the image file. "
764- "For example, 'us-west-1'."))
765-
766-
767-def filename_type(filename):
768- """An option validator for filenames.
769-
770- :raise: an error if 'filename' is not a file we can write to.
771- :return: 'filename' otherwise.
772- """
773- if filename is None:
774- return filename
775-
776- check_file = filename
777- if os.path.exists(check_file):
778- if not os.path.isfile(check_file):
779- raise BzrCommandError(
780- 'file argument %s exists and is not a file' % (filename,))
781- else:
782- check_file = os.path.dirname(check_file)
783- if (not os.path.exists(check_file) or
784- not os.path.isdir(check_file)):
785- raise BzrCommandError(
786- 'file %s cannot be created.' % (filename,))
787- if not os.access(check_file, os.W_OK):
788- raise BzrCommandError(
789- 'you do not have permission to write %s' % (filename,))
790- return filename
791-
792-
793-def set_trace_if(enable_debugger=False):
794- """If `enable_debugger` is True, drop into the debugger."""
795- if enable_debugger:
796- pdb.set_trace()
797-
798-
799-class EC2Command(Command):
800- """Subclass of `Command` that customizes usage to say 'ec2' not 'bzr'.
801-
802- When https://bugs.edge.launchpad.net/bzr/+bug/431054 is fixed, we can
803- delete this class, or at least make it less of a copy/paste/hack of the
804- superclass.
805- """
806-
807- def _usage(self):
808- """Return single-line grammar for this command.
809-
810- Only describes arguments, not options.
811- """
812- s = 'ec2 ' + self.name() + ' '
813- for aname in self.takes_args:
814- aname = aname.upper()
815- if aname[-1] in ['$', '+']:
816- aname = aname[:-1] + '...'
817- elif aname[-1] == '?':
818- aname = '[' + aname[:-1] + ']'
819- elif aname[-1] == '*':
820- aname = '[' + aname[:-1] + '...]'
821- s += aname + ' '
822- s = s[:-1] # remove last space
823- return s
824-
825-
826-def _get_branches_and_test_branch(trunk, branch, test_branch):
827- """Interpret the command line options to find which branch to test.
828-
829- :param trunk: The value of the --trunk option.
830- :param branch: The value of the --branch options.
831- :param test_branch: The value of the TEST_BRANCH argument.
832- """
833- if trunk:
834- if test_branch is not None:
835- raise BzrCommandError(
836- "Cannot specify both a branch to test and --trunk")
837- else:
838- test_branch = TRUNK_BRANCH
839- else:
840- if test_branch is None:
841- test_branch = '.'
842- branches = [data.split('=', 1) for data in branch]
843- return branches, test_branch
844-
845-
846-DEFAULT_TEST_OPTIONS = '--subunit -vvv'
847-
848-
849-class cmd_test(EC2Command):
850- """Run the test suite in ec2."""
851-
852- takes_options = [
853- branch_option,
854- trunk_option,
855- machine_id_option,
856- instance_type_option,
857- region_option,
858- Option(
859- 'file', short_name='f', type=filename_type,
860- help=('Store abridged test results in FILE.')),
861- ListOption(
862- 'email', short_name='e', argname='EMAIL', type=str,
863- help=('Email address to which results should be mailed. '
864- 'Defaults to the email address from `bzr whoami`. May be '
865- 'supplied multiple times. `bzr whoami` will be used as '
866- 'the From: address.')),
867- Option(
868- 'noemail', short_name='n',
869- help=('Do not try to email results.')),
870- Option(
871- 'test-options', short_name='o', type=str,
872- help=('Test options to pass to the remote test runner. Defaults '
873- "to ``-o '-vv'``. For instance, to run specific tests, "
874- "you might use ``-o '-vvt my_test_pattern'``.")),
875- Option(
876- 'submit-pqm-message', short_name='s', type=str, argname="MSG",
877- help=(
878- 'A pqm message to submit if the test run is successful. If '
879- 'provided, you will be asked for your GPG passphrase before '
880- 'the test run begins.')),
881- Option(
882- 'pqm-public-location', type=str,
883- help=('The public location for the pqm submit, if a pqm message '
884- 'is provided (see --submit-pqm-message). If this is not '
885- 'provided, for local branches, bzr configuration is '
886- 'consulted; for remote branches, it is assumed that the '
887- 'remote branch *is* a public branch.')),
888- Option(
889- 'pqm-submit-location', type=str,
890- help=('The submit location for the pqm submit, if a pqm message '
891- 'is provided (see --submit-pqm-message). If this option '
892- 'is not provided, the script will look for an explicitly '
893- 'specified launchpad branch using the -b/--branch option; '
894- 'if that branch was specified and is owned by the '
895- 'launchpad-pqm user on launchpad, it is used as the pqm '
896- 'submit location. Otherwise, for local branches, bzr '
897- 'configuration is consulted; for remote branches, it is '
898- 'assumed that the submit branch is %s.'
899- % (TRUNK_BRANCH,))),
900- Option(
901- 'pqm-email', type=str,
902- help=(
903- 'Specify the email address of the PQM you are submitting to. '
904- 'If the branch is local, then the bzr configuration is '
905- 'consulted; for remote branches "Launchpad PQM '
906- '<launchpad@pqm.canonical.com>" is used by default.')),
907- postmortem_option,
908- attached_option,
909- debug_option,
910- Option(
911- 'open-browser',
912- help=('Open the results page in your default browser')),
913- include_download_cache_changes_option,
914- ]
915-
916- takes_args = ['test_branch?']
917-
918- def run(self, test_branch=None, branch=None, trunk=False, machine=None,
919- instance_type=DEFAULT_INSTANCE_TYPE,
920- file=None, email=None, test_options=DEFAULT_TEST_OPTIONS,
921- noemail=False, submit_pqm_message=None, pqm_public_location=None,
922- pqm_submit_location=None, pqm_email=None, postmortem=False,
923- attached=False, debug=False, open_browser=False,
924- region=None,
925- include_download_cache_changes=False):
926- set_trace_if(debug)
927- if branch is None:
928- branch = []
929- branches, test_branch = _get_branches_and_test_branch(
930- trunk, branch, test_branch)
931- if (postmortem or file):
932- attached = True
933- if noemail:
934- if email:
935- raise BzrCommandError(
936- 'May not supply both --no-email and an --email address')
937- else:
938- if email == []:
939- email = True
940-
941- if not attached and not (email or submit_pqm_message):
942- raise BzrCommandError(
943- 'You have specified no way to get the results '
944- 'of your headless test run.')
945-
946- if (test_options != DEFAULT_TEST_OPTIONS
947- and submit_pqm_message is not None):
948- raise BzrCommandError(
949- "Submitting to PQM with non-default test options isn't "
950- "supported")
951-
952- session_name = EC2SessionName.make(EC2TestRunner.name)
953- instance = EC2Instance.make(session_name, instance_type, machine,
954- region=region)
955-
956- runner = EC2TestRunner(
957- test_branch, email=email, file=file,
958- test_options=test_options, headless=(not attached),
959- branches=branches, pqm_message=submit_pqm_message,
960- pqm_public_location=pqm_public_location,
961- pqm_submit_location=pqm_submit_location,
962- open_browser=open_browser, pqm_email=pqm_email,
963- include_download_cache_changes=include_download_cache_changes,
964- instance=instance, launchpad_login=instance._launchpad_login,
965- timeout=480)
966-
967- instance.set_up_and_run(postmortem, attached, runner.run_tests)
968-
969-
970-class cmd_land(EC2Command):
971- """Land a merge proposal on Launchpad."""
972-
973- takes_options = [
974- debug_option,
975- instance_type_option,
976- region_option,
977- machine_id_option,
978- Option('dry-run', help="Just print the equivalent ec2 test command."),
979- Option('print-commit', help="Print the full commit message."),
980- Option(
981- 'testfix',
982- help="This is a testfix (tags commit with [testfix])."),
983- Option(
984- 'no-qa',
985- help="Does not require QA (tags commit with [no-qa])."),
986- Option(
987- 'incremental',
988- help="Incremental to other bug fix (tags commit with [incr])."),
989- Option(
990- 'rollback', type=int,
991- help=(
992- "Rollback given revision number. (tags commit with "
993- "[rollback=revno]).")),
994- Option(
995- 'commit-text', short_name='s', type=str,
996- help=(
997- 'A description of the landing, not including reviewer '
998- 'metadata etc.')),
999- Option(
1000- 'force',
1001- help="Land the branch even if the proposal is not approved."),
1002- attached_option,
1003- ]
1004-
1005- takes_args = ['merge_proposal?']
1006-
1007- def _get_landing_command(self, source_url, target_url, commit_message,
1008- emails, attached):
1009- """Return the command that would need to be run to submit with ec2."""
1010- ec2_path = os.path.join(get_launchpad_root(), 'utilities', 'ec2')
1011- command = [ec2_path, 'test']
1012- if attached:
1013- command.extend(['--attached'])
1014- command.extend(['--email=%s' % email for email in emails])
1015- # 'ec2 test' has a bug where you cannot pass full URLs to branches to
1016- # the -b option. It has special logic for 'launchpad' branches, so we
1017- # piggy back on this to get 'devel' or 'db-devel'.
1018- target_branch_name = target_url.split('/')[-1]
1019- command.extend(
1020- ['-b', 'launchpad=%s' % (target_branch_name), '-s',
1021- commit_message, str(source_url)])
1022- return command
1023-
1024- def run(self, merge_proposal=None, machine=None,
1025- instance_type=DEFAULT_INSTANCE_TYPE, postmortem=False,
1026- debug=False, commit_text=None, dry_run=False, testfix=False,
1027- no_qa=False, incremental=False, rollback=None, print_commit=False,
1028- force=False, attached=False,
1029- region=DEFAULT_REGION,
1030- ):
1031- try:
1032- from devscripts.autoland import (
1033- LaunchpadBranchLander, MissingReviewError, MissingBugsError,
1034- MissingBugsIncrementalError)
1035- except ImportError:
1036- self.outf.write(
1037- "***************************************************\n\n"
1038- "Could not load the autoland module; please ensure\n"
1039- "that launchpadlib and lazr.uri are installed and\n"
1040- "found in sys.path/PYTHONPATH.\n\n"
1041- "Note that these should *not* be installed system-\n"
1042- "wide because this will break the rest of Launchpad.\n\n"
1043- "***************************************************\n")
1044- raise
1045- set_trace_if(debug)
1046- if print_commit and dry_run:
1047- raise BzrCommandError(
1048- "Cannot specify --print-commit and --dry-run.")
1049- lander = LaunchpadBranchLander.load()
1050-
1051- if merge_proposal is None:
1052- (tree, bzrbranch, relpath) = (
1053- BzrDir.open_containing_tree_or_branch('.'))
1054- mp = lander.get_merge_proposal_from_branch(bzrbranch)
1055- else:
1056- mp = lander.load_merge_proposal(merge_proposal)
1057- if not mp.is_approved:
1058- if force:
1059- print "Merge proposal is not approved, landing anyway."
1060- else:
1061- raise BzrCommandError(
1062- "Merge proposal is not approved. Get it approved, or use "
1063- "--force to land it without approval.")
1064- if commit_text is None:
1065- commit_text = mp.commit_message
1066- if commit_text is None:
1067- raise BzrCommandError(
1068- "Commit text not specified. Use --commit-text, or specify a "
1069- "message on the merge proposal.")
1070- if rollback and (no_qa or incremental):
1071- print (
1072- "--rollback option used. Ignoring --no-qa and --incremental.")
1073- try:
1074- commit_message = mp.build_commit_message(
1075- commit_text, testfix, no_qa, incremental, rollback=rollback)
1076- except MissingReviewError:
1077- raise BzrCommandError(
1078- "Cannot land branches that haven't got approved code "
1079- "reviews. Get an 'Approved' vote so we can fill in the "
1080- "[r=REVIEWER] section.")
1081- except MissingBugsError:
1082- raise BzrCommandError(
1083- "Branch doesn't have linked bugs and doesn't have no-qa "
1084- "option set. Use --no-qa, or link the related bugs to the "
1085- "branch.")
1086- except MissingBugsIncrementalError:
1087- raise BzrCommandError(
1088- "--incremental option requires bugs linked to the branch. "
1089- "Link the bugs or remove the --incremental option.")
1090-
1091- # Override the commit message in the MP with the commit message built
1092- # with the proper tags.
1093- try:
1094- mp.set_commit_message(commit_message)
1095- except Exception, e:
1096- raise BzrCommandError(
1097- "Unable to set the commit message in the merge proposal.\n"
1098- "Got: %s" % e)
1099-
1100- if print_commit:
1101- print commit_message
1102- return
1103-
1104- emails = mp.get_stakeholder_emails()
1105-
1106- target_branch_name = mp.target_branch.split('/')[-1]
1107- branches = [('launchpad', target_branch_name)]
1108-
1109- landing_command = self._get_landing_command(
1110- mp.source_branch, mp.target_branch, commit_message,
1111- emails, attached)
1112-
1113- if dry_run:
1114- print landing_command
1115- return
1116-
1117- session_name = EC2SessionName.make(EC2TestRunner.name)
1118- instance = EC2Instance.make(
1119- session_name, instance_type, machine, region=region)
1120-
1121- runner = EC2TestRunner(
1122- mp.source_branch, email=emails,
1123- headless=(not attached),
1124- branches=branches, pqm_message=commit_message,
1125- instance=instance,
1126- launchpad_login=instance._launchpad_login,
1127- test_options=DEFAULT_TEST_OPTIONS,
1128- timeout=480)
1129-
1130- instance.set_up_and_run(postmortem, attached, runner.run_tests)
1131-
1132-
1133-class cmd_demo(EC2Command):
1134- """Start a demo instance of Launchpad.
1135-
1136- See https://wiki.canonical.com/Launchpad/EC2Test/ForDemos
1137- """
1138-
1139- takes_options = [
1140- branch_option,
1141- trunk_option,
1142- machine_id_option,
1143- instance_type_option,
1144- postmortem_option,
1145- debug_option,
1146- include_download_cache_changes_option,
1147- region_option,
1148- ListOption(
1149- 'demo', type=str,
1150- help="Allow this netmask to connect to the instance."),
1151- ]
1152-
1153- takes_args = ['test_branch?']
1154-
1155- def run(self, test_branch=None, branch=None, trunk=False, machine=None,
1156- instance_type=DEFAULT_INSTANCE_TYPE, debug=False,
1157- include_download_cache_changes=False, demo=None):
1158- set_trace_if(debug)
1159- if branch is None:
1160- branch = []
1161- branches, test_branch = _get_branches_and_test_branch(
1162- trunk, branch, test_branch)
1163-
1164- session_name = EC2SessionName.make(EC2TestRunner.name)
1165- instance = EC2Instance.make(
1166- session_name, instance_type, machine, demo)
1167-
1168- runner = EC2TestRunner(
1169- test_branch, branches=branches,
1170- include_download_cache_changes=include_download_cache_changes,
1171- instance=instance, launchpad_login=instance._launchpad_login)
1172-
1173- demo_network_string = '\n'.join(
1174- ' ' + network for network in demo)
1175-
1176- # Wait until the user exits the postmortem session, then kill the
1177- # instance.
1178- postmortem = True
1179- shutdown = True
1180- instance.set_up_and_run(
1181- postmortem, shutdown, self.run_server, runner, instance,
1182- demo_network_string)
1183-
1184- def run_server(self, runner, instance, demo_network_string):
1185- runner.run_demo_server()
1186- ec2_ip = socket.gethostbyname(instance.hostname)
1187- print (
1188- "\n\n"
1189- "********************** DEMO *************************\n"
1190- "It may take 20 seconds for the demo server to start up."
1191- "\nTo demo to other users, you still need to open up\n"
1192- "network access to the ec2 instance from their IPs by\n"
1193- "entering command like this in the interactive python\n"
1194- "interpreter at the end of the setup. "
1195- "\n self.security_group.authorize("
1196- "'tcp', 443, 443, '10.0.0.5/32')\n\n"
1197- "These demo networks have already been granted access on "
1198- "port 80 and 443:\n" + demo_network_string +
1199- "\n\nYou also need to edit your /etc/hosts to point\n"
1200- "launchpad.dev at the ec2 instance's IP like this:\n"
1201- " " + ec2_ip + " launchpad.dev\n\n"
1202- "See "
1203- "<https://wiki.canonical.com/Launchpad/EC2Test/ForDemos>."
1204- "\n*****************************************************"
1205- "\n\n")
1206-
1207-
1208-class cmd_update_image(EC2Command):
1209- """Make a new AMI."""
1210-
1211- takes_options = [
1212- machine_id_option,
1213- instance_type_option,
1214- postmortem_option,
1215- debug_option,
1216- region_option,
1217- ListOption(
1218- 'extra-update-image-command', type=str,
1219- help=('Run this command (with an ssh agent) on the image before '
1220- 'running the default update steps. Can be passed more '
1221- 'than once, the commands will be run in the order '
1222- 'specified.')),
1223- Option(
1224- 'public',
1225- help=('Remove proprietary code from the sourcecode directory '
1226- 'before bundling.')),
1227- ]
1228-
1229- takes_args = ['ami_name']
1230-
1231- def run(self, ami_name, machine=None, instance_type='m1.large',
1232- debug=False, postmortem=False, extra_update_image_command=None,
1233- region=None,
1234- public=False):
1235- set_trace_if(debug)
1236-
1237- if extra_update_image_command is None:
1238- extra_update_image_command = []
1239-
1240- # These environment variables are passed through ssh connections to
1241- # fresh Ubuntu images and cause havoc if the locales they refer to are
1242- # not available. We kill them here to ease bootstrapping, then we
1243- # later modify the image to prevent sshd from accepting them.
1244- for variable in ['LANG', 'LC_ALL', 'LC_TIME']:
1245- os.environ.pop(variable, None)
1246-
1247- session_name = EC2SessionName.make(EC2TestRunner.name)
1248- instance = EC2Instance.make(
1249- session_name, instance_type, machine,
1250- region=region)
1251- instance.check_bundling_prerequisites(ami_name)
1252- instance.set_up_and_run(
1253- postmortem, True, self.update_image, instance,
1254- extra_update_image_command, ami_name, instance._credentials,
1255- public)
1256-
1257- def update_image(self, instance, extra_update_image_command, ami_name,
1258- credentials, public):
1259- """Bring the image up to date.
1260-
1261- The steps we take are:
1262-
1263- * run any commands specified with --extra-update-image-command
1264- * update sourcecode
1265- * update the launchpad branch to the tip of the trunk branch.
1266- * update the copy of the download-cache.
1267- * bundle the image
1268-
1269- :param instance: `EC2Instance` to operate on.
1270- :param extra_update_image_command: List of commands to run on the
1271- instance in addition to the usual ones.
1272- :param ami_name: The name to give the created AMI.
1273- :param credentials: An `EC2Credentials` object.
1274- :param public: If true, remove proprietary code from the sourcecode
1275- directory before bundling.
1276- """
1277- # Do NOT accept environment variables via ssh connections.
1278- user_connection = instance.connect()
1279- user_connection.perform('sudo apt-get -qqy update')
1280- user_connection.perform('sudo apt-get -qqy upgrade')
1281- user_connection.perform(
1282- 'sudo sed -i "s/^AcceptEnv/#AcceptEnv/" /etc/ssh/sshd_config')
1283- user_connection.perform(
1284- 'sudo kill -HUP $(< /var/run/sshd.pid)')
1285- # Reconnect to ensure that the environment is clean.
1286- user_connection.reconnect()
1287- user_connection.perform(
1288- 'bzr launchpad-login %s' % (instance._launchpad_login,))
1289- for cmd in extra_update_image_command:
1290- user_connection.run_with_ssh_agent(cmd)
1291- user_connection.run_with_ssh_agent(
1292- 'bzr pull -d /var/launchpad/test ' + TRUNK_BRANCH)
1293- user_connection.run_with_ssh_agent(
1294- 'bzr pull -d /var/launchpad/download-cache '
1295- 'lp:lp-source-dependencies')
1296- if public:
1297- update_sourcecode_options = ' --public-only'
1298- else:
1299- update_sourcecode_options = ''
1300- user_connection.run_with_ssh_agent(
1301- "/var/launchpad/test/utilities/update-sourcecode "
1302- "/var/launchpad/sourcecode" + update_sourcecode_options)
1303- user_connection.perform(
1304- 'rm -rf .ssh/known_hosts .bazaar .bzr.log')
1305- user_connection.close()
1306- instance.bundle(ami_name, credentials)
1307-
1308-
1309-class cmd_images(EC2Command):
1310- """Display all available images.
1311-
1312- The first in the list is the default image.
1313- """
1314-
1315- takes_options = [
1316- region_option,
1317- ]
1318-
1319- def run(self, region=None):
1320- session_name = EC2SessionName.make(EC2TestRunner.name)
1321- credentials = EC2Credentials.load_from_file(region_name=region)
1322- account = credentials.connect(session_name)
1323- format = "%5s %-12s %-12s %-12s %s\n"
1324- self.outf.write(
1325- format % ("Rev", "AMI", "Owner ID", "Owner", "Description"))
1326- for revision, images in account.find_images():
1327- for image in images:
1328- self.outf.write(format % (
1329- revision, image.id, image.ownerId,
1330- VALID_AMI_OWNERS.get(image.ownerId, "unknown"),
1331- image.description or ''))
1332-
1333-
1334-class cmd_kill(EC2Command):
1335- """Kill one or more running EC2 instances.
1336-
1337- You can get the instance id from 'ec2 list'.
1338- """
1339-
1340- takes_options = [
1341- region_option,
1342- ]
1343- takes_args = ['instance_id*']
1344-
1345- def run(self, instance_id_list, region=None):
1346- credentials = EC2Credentials.load_from_file(region_name=region)
1347- account = credentials.connect('ec2 kill')
1348- self.outf.write("killing %d instances: " % len(instance_id_list,))
1349- account.conn.terminate_instances(instance_id_list)
1350- self.outf.write("done\n")
1351-
1352-
1353-class cmd_list(EC2Command):
1354- """List all your current EC2 test runs.
1355-
1356- If an instance is publishing an 'info.json' file with 'description' and
1357- 'failed-yet' fields, this command will list that instance, whether it has
1358- failed the test run and how long it has been up for.
1359-
1360- [FAILED] means that the has been a failing test. [OK] means that the test
1361- run has had no failures yet, it's not a guarantee of a successful run.
1362- """
1363-
1364- aliases = ["ls"]
1365-
1366- takes_options = [
1367- region_option,
1368- Option('show-urls',
1369- help="Include more information about each instance"),
1370- Option('all', short_name='a',
1371- help="Show all instances, not just ones with ec2test data."),
1372- ]
1373-
1374- def iter_instances(self, account):
1375- """Iterate through all instances in 'account'."""
1376- for reservation in account.conn.get_all_instances():
1377- for instance in reservation.instances:
1378- yield instance
1379-
1380- def get_uptime(self, instance):
1381- """How long has 'instance' been running?"""
1382- expected_format = '%Y-%m-%dT%H:%M:%S.000Z'
1383- launch_time = datetime.strptime(instance.launch_time, expected_format)
1384- delta = (
1385- datetime.utcnow().replace(tzinfo=UTC)
1386- - launch_time.replace(tzinfo=UTC))
1387- return timedelta(delta.days, delta.seconds) # Round it.
1388-
1389- def get_http_url(self, instance):
1390- hostname = instance.public_dns_name
1391- if not hostname:
1392- return
1393- return 'http://%s/' % (hostname,)
1394-
1395- def get_ec2test_info(self, instance):
1396- """Load the ec2test-specific information published by 'instance'."""
1397- url = self.get_http_url(instance)
1398- if url is None:
1399- return
1400- try:
1401- json = get_transport(url).get_bytes('info.json')
1402- except (ConnectionError, NoSuchFile):
1403- # Probably not an ec2test instance, or not ready yet.
1404- return None
1405- return simplejson.loads(json)
1406-
1407- def format_instance(self, instance, data, verbose):
1408- """Format 'instance' for display.
1409-
1410- :param instance: The EC2 instance to display.
1411- :param data: Launchpad-specific data.
1412- :param verbose: Whether we want verbose output.
1413- """
1414- description = instance.id
1415- uptime = self.get_uptime(instance)
1416- if instance.state != 'running':
1417- current_status = instance.state
1418- else:
1419- if data is None:
1420- current_status = 'unknown '
1421- else:
1422- description = data['description']
1423- if data['failed-yet']:
1424- current_status = '[FAILED]'
1425- else:
1426- current_status = '[OK] '
1427- output = (
1428- '%-40s %-10s (up for %s) %10s'
1429- % (description, current_status, uptime, instance.id))
1430- if verbose:
1431- url = self.get_http_url(instance)
1432- if url is None:
1433- url = "No web service"
1434- output += '\n %s' % (url,)
1435- if instance.state_reason:
1436- output += (
1437- '\n transition reason: %s'
1438- % instance.state_reason.get('message', ''))
1439- return output
1440-
1441- def format_summary(self, by_state):
1442- return ', '.join(
1443- ': '.join((state, str(num)))
1444- for (state, num) in sorted(list(by_state.items())))
1445-
1446- def run(self, show_urls=False, all=False, region=None):
1447- session_name = EC2SessionName.make(EC2TestRunner.name)
1448- credentials = EC2Credentials.load_from_file(region_name=region)
1449- account = credentials.connect(session_name)
1450- instances = list(self.iter_instances(account))
1451- if len(instances) == 0:
1452- print "No instances running."
1453- return
1454-
1455- by_state = {}
1456- for instance in instances:
1457- by_state[instance.state] = by_state.get(instance.state, 0) + 1
1458- data = self.get_ec2test_info(instance)
1459- if data is None and not all:
1460- continue
1461- print self.format_instance(
1462- instance, data, verbose=(show_urls or is_verbose()))
1463- print 'Summary: %s' % (self.format_summary(by_state),)
1464-
1465-
1466-class cmd_help(EC2Command):
1467- """Show general help or help for a command."""
1468-
1469- aliases = ["?", "--help", "-?", "-h"]
1470- takes_args = ["topic?"]
1471-
1472- def run(self, topic=None):
1473- """
1474- Show help for the C{bzrlib.commands.Command} matching C{topic}.
1475-
1476- @param topic: Optionally, the name of the topic to show. Default is
1477- to show some basic usage information.
1478- """
1479- if topic is None:
1480- self.outf.write('Usage: ec2 <command> <options>\n\n')
1481- self.outf.write('Available commands:\n')
1482- help_commands(self.outf)
1483- else:
1484- command = self.controller._get_command(None, topic)
1485- if command is None:
1486- self.outf.write("%s is an unknown command.\n" % (topic,))
1487- text = command.get_help_text()
1488- if text:
1489- self.outf.write(text)
1490
1491=== removed file 'lib/devscripts/ec2test/controller.py'
1492--- lib/devscripts/ec2test/controller.py 2012-01-01 03:03:28 +0000
1493+++ lib/devscripts/ec2test/controller.py 1970-01-01 00:00:00 +0000
1494@@ -1,181 +0,0 @@
1495-# This file is incuded almost verbatim from commandant,
1496-# https://launchpad.net/commandant. The only changes are removing some code
1497-# we don't use that depends on other parts of commandant. When Launchpad is
1498-# on Python 2.5 we can include commandant as an egg.
1499-
1500-
1501-# Commandant is a framework for building command-oriented tools.
1502-# Copyright (C) 2009 Jamshed Kakar.
1503-#
1504-# This program is free software; you can redistribute it and/or modify
1505-# it under the terms of the GNU General Public License as published by
1506-# the Free Software Foundation; either version 2 of the License, or
1507-# (at your option) any later version.
1508-#
1509-# This program is distributed in the hope that it will be useful,
1510-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1511-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1512-# GNU General Public License for more details.
1513-#
1514-# You should have received a copy of the GNU General Public License along
1515-# with this program; if not, write to the Free Software Foundation, Inc.,
1516-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
1517-
1518-"""Infrastructure to run C{bzrlib.commands.Command}s and L{HelpTopic}s."""
1519-
1520-import os
1521-import sys
1522-
1523-from bzrlib.commands import (
1524- Command,
1525- run_bzr,
1526- )
1527-
1528-
1529-class CommandRegistry(object):
1530-
1531- def __init__(self):
1532- self._commands = {}
1533-
1534- def install_bzrlib_hooks(self):
1535- """
1536- Register this controller with C{Command.hooks} so that the controller
1537- can take advantage of Bazaar's command infrastructure.
1538-
1539- L{_list_commands} and L{_get_command} are registered as callbacks for
1540- the C{list_commands} and C{get_commands} hooks, respectively.
1541- """
1542- Command.hooks.install_named_hook(
1543- "list_commands", self._list_commands, "commandant commands")
1544- Command.hooks.install_named_hook(
1545- "get_command", self._get_command, "commandant commands")
1546-
1547- def _list_commands(self, names):
1548- """Hook to find C{bzrlib.commands.Command} names is called by C{bzrlib}.
1549-
1550- @param names: A set of C{bzrlib.commands.Command} names to update with
1551- names from this controller.
1552- """
1553- names.update(self._commands.iterkeys())
1554- return names
1555-
1556- def _get_command(self, command, name):
1557- """
1558- Hook to get the C{bzrlib.commands.Command} for C{name} is called by
1559- C{bzrlib}.
1560-
1561- @param command: A C{bzrlib.commands.Command}, or C{None}, to be
1562- returned if a command matching C{name} can't be found.
1563- @param name: The name of the C{bzrlib.commands.Command} to retrieve.
1564- @return: The C{bzrlib.commands.Command} from the index or C{command}
1565- if one isn't available for C{name}.
1566- """
1567- try:
1568- local_command = self._commands[name]()
1569- except KeyError:
1570- for cmd in self._commands.itervalues():
1571- if name in cmd.aliases:
1572- local_command = cmd()
1573- break
1574- else:
1575- return command
1576- local_command.controller = self
1577- return local_command
1578-
1579- def register_command(self, name, command_class):
1580- """Register a C{bzrlib.commands.Command} with this controller.
1581-
1582- @param name: The name to register the command with.
1583- @param command_class: A type object, typically a subclass of
1584- C{bzrlib.commands.Command} to use when the command is invoked.
1585- """
1586- self._commands[name] = command_class
1587-
1588- def load_module(self, module):
1589- """Load C{bzrlib.commands.Command}s and L{HelpTopic}s from C{module}.
1590-
1591- Objects found in the module with names that start with C{cmd_} are
1592- treated as C{bzrlib.commands.Command}s and objects with names that
1593- start with C{topic_} are treated as L{HelpTopic}s.
1594- """
1595- for name in module.__dict__:
1596- if name.startswith("cmd_"):
1597- sanitized_name = name[4:].replace("_", "-")
1598- self.register_command(sanitized_name, module.__dict__[name])
1599- elif name.startswith("topic_"):
1600- sanitized_name = name[6:].replace("_", "-")
1601- self.register_help_topic(sanitized_name, module.__dict__[name])
1602-
1603-
1604-class HelpTopicRegistry(object):
1605-
1606- def __init__(self):
1607- self._help_topics = {}
1608-
1609- def register_help_topic(self, name, help_topic_class):
1610- """Register a C{bzrlib.commands.Command} to this controller.
1611-
1612- @param name: The name to register the command with.
1613- @param command_class: A type object, typically a subclass of
1614- C{bzrlib.commands.Command} to use when the command is invoked.
1615- """
1616- self._help_topics[name] = help_topic_class
1617-
1618- def get_help_topic_names(self):
1619- """Get a C{set} of help topic names."""
1620- return set(self._help_topics.iterkeys())
1621-
1622- def get_help_topic(self, name):
1623- """
1624- Get the help topic matching C{name} or C{None} if a match isn't found.
1625- """
1626- try:
1627- help_topic = self._help_topics[name]()
1628- except KeyError:
1629- return None
1630- help_topic.controller = self
1631- return help_topic
1632-
1633-
1634-
1635-class CommandExecutionMixin(object):
1636-
1637- def run(self, argv):
1638- """Run the C{bzrlib.commands.Command} specified in C{argv}.
1639-
1640- @raise BzrCommandError: Raised if a matching command can't be found.
1641- """
1642- run_bzr(argv)
1643-
1644-
1645-
1646-def import_module(filename, file_path, package_path):
1647- """Import a module and make it a child of C{commandant_command}.
1648-
1649- The module source in C{filename} at C{file_path} is copied to a temporary
1650- directory, a Python package called C{commandant_command}.
1651-
1652- @param filename: The name of the module file.
1653- @param file_path: The path to the module file.
1654- @param package_path: The path for the new C{commandant_command} package.
1655- @return: The new module.
1656- """
1657- module_path = os.path.join(package_path, "commandant_command")
1658- if not os.path.exists(module_path):
1659- os.mkdir(module_path)
1660-
1661- init_path = os.path.join(module_path, "__init__.py")
1662- open(init_path, "w").close()
1663-
1664- source_code = open(file_path, "r").read()
1665- module_file_path = os.path.join(module_path, filename)
1666- module_file = open(module_file_path, "w")
1667- module_file.write(source_code)
1668- module_file.close()
1669-
1670- name = filename[:-3]
1671- sys.path.append(package_path)
1672- try:
1673- return __import__("commandant_command.%s" % (name,), fromlist=[name])
1674- finally:
1675- sys.path.pop()
1676
1677=== removed file 'lib/devscripts/ec2test/credentials.py'
1678--- lib/devscripts/ec2test/credentials.py 2012-01-01 03:03:28 +0000
1679+++ lib/devscripts/ec2test/credentials.py 1970-01-01 00:00:00 +0000
1680@@ -1,76 +0,0 @@
1681-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
1682-# GNU Affero General Public License version 3 (see the file LICENSE).
1683-
1684-"""Support for reading Amazon Web Service credentials from '~/.ec2/aws_id'."""
1685-
1686-__metaclass__ = type
1687-__all__ = [
1688- 'CredentialsError',
1689- 'EC2Credentials',
1690- ]
1691-
1692-import os
1693-
1694-import boto
1695-import boto.ec2
1696-from bzrlib.errors import BzrCommandError
1697-
1698-from devscripts.ec2test import instance
1699-from devscripts.ec2test.account import EC2Account
1700-
1701-
1702-class CredentialsError(BzrCommandError):
1703- """Raised when AWS credentials could not be loaded."""
1704-
1705- _fmt = (
1706- "Please put your aws access key identifier and secret access "
1707- "key identifier in %(filename)s. (On two lines). %(extra)s")
1708-
1709- def __init__(self, filename, extra=None):
1710- super(CredentialsError, self).__init__(filename=filename, extra=extra)
1711-
1712-
1713-class EC2Credentials:
1714- """Credentials for logging in to EC2."""
1715-
1716- DEFAULT_CREDENTIALS_FILE = '~/.ec2/aws_id'
1717-
1718- def __init__(self, identifier, secret, region_name):
1719- self.identifier = identifier
1720- self.secret = secret
1721- self.region_name = region_name or instance.DEFAULT_REGION
1722-
1723- @classmethod
1724- def load_from_file(cls, filename=None, region_name=None):
1725- """Load the EC2 credentials from 'filename'."""
1726- if filename is None:
1727- filename = os.path.expanduser(cls.DEFAULT_CREDENTIALS_FILE)
1728- try:
1729- aws_file = open(filename, 'r')
1730- except (IOError, OSError), e:
1731- raise CredentialsError(filename, str(e))
1732- try:
1733- identifier = aws_file.readline().strip()
1734- secret = aws_file.readline().strip()
1735- finally:
1736- aws_file.close()
1737- return cls(identifier, secret, region_name)
1738-
1739- def connect(self, name):
1740- """Connect to EC2 with these credentials.
1741-
1742- :param name: Arbitrary local name for the object.
1743- :return: An `EC2Account` connected to EC2 with these credentials.
1744- """
1745- conn = boto.ec2.connect_to_region(
1746- self.region_name,
1747- aws_access_key_id=self.identifier,
1748- aws_secret_access_key=self.secret)
1749- return EC2Account(name, conn)
1750-
1751- def connect_s3(self):
1752- """Connect to S3 with these credentials.
1753-
1754- :return: A `boto.s3.connection.S3Connection` with these credentials.
1755- """
1756- return boto.connect_s3(self.identifier, self.secret)
1757
1758=== removed file 'lib/devscripts/ec2test/entrypoint.py'
1759--- lib/devscripts/ec2test/entrypoint.py 2012-01-01 03:03:28 +0000
1760+++ lib/devscripts/ec2test/entrypoint.py 1970-01-01 00:00:00 +0000
1761@@ -1,49 +0,0 @@
1762-# Copyright 2009 Canonical Ltd. This software is licensed under the
1763-# GNU Affero General Public License version 3 (see the file LICENSE).
1764-
1765-"""The entry point for the 'ec2' utility."""
1766-
1767-__metaclass__ = type
1768-__all__ = [
1769- 'main',
1770- ]
1771-
1772-import readline
1773-import rlcompleter
1774-import sys
1775-
1776-import bzrlib
1777-from bzrlib.errors import BzrCommandError
1778-
1779-from devscripts.ec2test import builtins
1780-from devscripts.ec2test.controller import (
1781- CommandExecutionMixin,
1782- CommandRegistry,
1783- )
1784-
1785-# Shut up pyflakes.
1786-rlcompleter
1787-
1788-readline.parse_and_bind('tab: complete')
1789-
1790-class EC2CommandController(CommandRegistry, CommandExecutionMixin):
1791- """The 'ec2' utility registers and executes commands."""
1792-
1793-
1794-def main():
1795- """The entry point for the 'ec2' script.
1796-
1797- We run the specified command, or give help if none was specified.
1798- """
1799- with bzrlib.initialize():
1800- controller = EC2CommandController()
1801- controller.install_bzrlib_hooks()
1802- controller.load_module(builtins)
1803-
1804- args = sys.argv[1:]
1805- if not args:
1806- args = ['help']
1807- try:
1808- controller.run(args)
1809- except BzrCommandError, e:
1810- sys.exit('ec2: ERROR: ' + str(e))
1811
1812=== removed file 'lib/devscripts/ec2test/instance.py'
1813--- lib/devscripts/ec2test/instance.py 2012-02-07 14:53:38 +0000
1814+++ lib/devscripts/ec2test/instance.py 1970-01-01 00:00:00 +0000
1815@@ -1,742 +0,0 @@
1816-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1817-# GNU Affero General Public License version 3 (see the file LICENSE).
1818-
1819-"""Code to represent a single machine instance in EC2."""
1820-
1821-__metaclass__ = type
1822-__all__ = [
1823- 'EC2Instance',
1824- ]
1825-
1826-import code
1827-from datetime import datetime
1828-import errno
1829-import glob
1830-import os
1831-import select
1832-import socket
1833-import subprocess
1834-import sys
1835-import time
1836-import traceback
1837-
1838-from bzrlib.errors import BzrCommandError
1839-import paramiko
1840-
1841-from devscripts.ec2test.session import EC2SessionName
1842-
1843-
1844-DEFAULT_INSTANCE_TYPE = 'm2.xlarge'
1845-DEFAULT_REGION = 'us-east-1'
1846-AVAILABLE_INSTANCE_TYPES = (
1847- 'm1.large', 'm1.xlarge', 'm2.xlarge', 'm2.2xlarge', 'm2.4xlarge',
1848- 'c1.xlarge', 'cc1.4xlarge', 'cc1.8xlarge')
1849-
1850-
1851-class AcceptAllPolicy:
1852- """We accept all unknown host key."""
1853-
1854- def missing_host_key(self, client, hostname, key):
1855- # Normally the console output is supposed to contain the Host key but
1856- # it doesn't seem to be the case here, so we trust that the host we
1857- # are connecting to is the correct one.
1858- pass
1859-
1860-
1861-def get_user_key():
1862- """Get a SSH key from the agent. Raise an error if no keys were found.
1863-
1864- This key will be used to let the user log in (as $USER) to the instance.
1865- """
1866- agent = paramiko.Agent()
1867- keys = agent.get_keys()
1868- if len(keys) == 0:
1869- raise BzrCommandError(
1870- 'You must have an ssh agent running with keys installed that '
1871- 'will allow the script to access Launchpad and get your '
1872- 'branch.\n')
1873-
1874- # XXX mars 2010-05-07 bug=577118
1875- # Popping the first key off of the stack can create problems if the person
1876- # has more than one key in their ssh-agent, but alas, we have no good way
1877- # to detect the right key to use. See bug 577118 for a workaround.
1878- return keys[0]
1879-
1880-
1881-# Commands to run to turn a blank image into one usable for the rest of the
1882-# ec2 functionality. They come in two parts, one set that need to be run as
1883-# root and another that should be run as the 'ec2test' user.
1884-# Note that the sources from http://us.ec2.archive.ubuntu.com/ubuntu/ are per
1885-# instructions described in http://is.gd/g1MIT . When we switch to
1886-# Eucalyptus, we can dump this.
1887-
1888-from_scratch_root = """
1889-# From 'help set':
1890-# -x Print commands and their arguments as they are executed.
1891-# -e Exit immediately if a command exits with a non-zero status.
1892-set -xe
1893-
1894-# They end up as just one stream; this avoids ordering problems.
1895-exec 2>&1
1896-
1897-sed -ie 's/main universe/main universe multiverse/' /etc/apt/sources.list
1898-
1899-. /etc/lsb-release
1900-
1901-mount -o remount,data=writeback,commit=3600,async,relatime /
1902-
1903-cat >> /etc/apt/sources.list << EOF
1904-deb http://ppa.launchpad.net/launchpad/ubuntu $DISTRIB_CODENAME main
1905-deb http://ppa.launchpad.net/bzr/ubuntu $DISTRIB_CODENAME main
1906-EOF
1907-
1908-export DEBIAN_FRONTEND=noninteractive
1909-
1910-# PPA keys
1911-apt-key adv --recv-keys --keyserver pool.sks-keyservers.net 2af499cb24ac5f65461405572d1ffb6c0a5174af # launchpad
1912-apt-key adv --recv-keys --keyserver pool.sks-keyservers.net ece2800bacf028b31ee3657cd702bf6b8c6c1efd # bzr
1913-
1914-aptitude update
1915-
1916-# Do this first so later things don't complain about locales:
1917-LANG=C aptitude -y install language-pack-en
1918-
1919-aptitude -y full-upgrade
1920-
1921-# This next part is cribbed from rocketfuel-setup
1922-dev_host() {
1923- sed -i \"s/^127.0.0.88.*$/&\ ${hostname}/\" /etc/hosts
1924-}
1925-
1926-echo 'Adding development hosts on local machine'
1927-echo '
1928-# Launchpad virtual domains. This should be on one line.
1929-127.0.0.88 launchpad.dev
1930-' >> /etc/hosts
1931-
1932-declare -a hostnames
1933-hostnames=$(cat <<EOF
1934- answers.launchpad.dev
1935- api.launchpad.dev
1936- bazaar-internal.launchpad.dev
1937- beta.launchpad.dev
1938- blueprints.launchpad.dev
1939- bugs.launchpad.dev
1940- code.launchpad.dev
1941- feeds.launchpad.dev
1942- id.launchpad.dev
1943- keyserver.launchpad.dev
1944- lists.launchpad.dev
1945- openid.launchpad.dev
1946- ppa.launchpad.dev
1947- private-ppa.launchpad.dev
1948- testopenid.dev
1949- translations.launchpad.dev
1950- xmlrpc-private.launchpad.dev
1951- xmlrpc.launchpad.dev
1952-EOF
1953- )
1954-
1955-for hostname in $hostnames; do
1956- dev_host;
1957-done
1958-
1959-echo '
1960-127.0.0.99 bazaar.launchpad.dev
1961-' >> /etc/hosts
1962-
1963-# If this is uncommented, postgresql 9.1 will be explicitly installed
1964-# before other versisons and end up on port 5432, overriding the version
1965-# of postgresql specified in launchpad-database-dependencies.
1966-## apt-get -y install postgresql-9.1 postgresql-9.1-debversion postgresql-client-9.1 postgresql-contrib-9.1 postgresql-plpython-9.1 postgresql-server-dev-9.1 postgresql-doc-9.1
1967-apt-get -y install launchpad-developer-dependencies apache2 apache2-mpm-worker
1968-
1969-# Create the ec2test user, give them passwordless sudo.
1970-adduser --gecos "" --disabled-password ec2test
1971-echo 'ec2test\tALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
1972-
1973-mkdir /home/ec2test/.ssh
1974-cat > /home/ec2test/.ssh/config << EOF
1975-CheckHostIP no
1976-StrictHostKeyChecking no
1977-EOF
1978-
1979-mkdir /var/launchpad
1980-chown -R ec2test:ec2test /var/www /var/launchpad /home/ec2test/
1981-"""
1982-
1983-
1984-from_scratch_ec2test = """
1985-# From 'help set':
1986-# -x Print commands and their arguments as they are executed.
1987-# -e Exit immediately if a command exits with a non-zero status.
1988-set -xe
1989-
1990-# They end up as just one stream; this avoids ordering problems.
1991-exec 2>&1
1992-
1993-bzr launchpad-login %(launchpad-login)s
1994-bzr init-repo --2a /var/launchpad
1995-bzr branch lp:~launchpad-pqm/launchpad/devel /var/launchpad/test
1996-bzr branch --standalone lp:lp-source-dependencies /var/launchpad/download-cache
1997-mkdir /var/launchpad/sourcecode
1998-/var/launchpad/test/utilities/update-sourcecode /var/launchpad/sourcecode
1999-"""
2000-
2001-
2002-postmortem_banner = """\
2003-Postmortem Console. EC2 instance is not yet dead.
2004-It will shut down when you exit this prompt (CTRL-D)
2005-
2006-Tab-completion is enabled.
2007-EC2Instance is available as `instance`.
2008-Also try these:
2009- http://%(dns)s/current_test.log
2010- ssh -A ec2test@%(dns)s
2011-"""
2012-
2013-
2014-class EC2Instance:
2015- """A single EC2 instance."""
2016-
2017- @classmethod
2018- def make(cls, name, instance_type, machine_id, demo_networks=None,
2019- credentials=None, region=None):
2020- """Construct an `EC2Instance`.
2021-
2022- :param name: The name to use for the key pair and security group for
2023- the instance.
2024- :type name: `EC2SessionName`
2025- :param instance_type: One of the AVAILABLE_INSTANCE_TYPES.
2026- :param machine_id: The AMI to use, or None to do the usual regexp
2027- matching. If you put 'based-on:' before the AMI id, it is assumed
2028- that the id specifies a blank image that should be made into one
2029- suitable for the other ec2 functions (see `from_scratch_root` and
2030- `from_scratch_ec2test` above).
2031- :param demo_networks: A list of networks to add to the security group
2032- to allow access to the instance.
2033- :param credentials: An `EC2Credentials` object.
2034- :param region: A string region name eg 'us-east-1'.
2035- """
2036- # This import breaks in the test environment. Do it here so
2037- # that unit tests (which don't use this factory) can still
2038- # import EC2Instance.
2039- from bzrlib.plugins.launchpad.account import get_lp_login
2040-
2041- # XXX JeroenVermeulen 2009-11-27 bug=489073: EC2Credentials
2042- # imports boto, which isn't necessarily installed in our test
2043- # environment. Doing the import here so that unit tests (which
2044- # don't use this factory) can still import EC2Instance.
2045- from devscripts.ec2test.credentials import EC2Credentials
2046-
2047- assert isinstance(name, EC2SessionName)
2048-
2049- # We call this here so that it has a chance to complain before the
2050- # instance is started (which can take some time).
2051- user_key = get_user_key()
2052-
2053- if credentials is None:
2054- credentials = EC2Credentials.load_from_file(region_name=region)
2055-
2056- # Make the EC2 connection.
2057- account = credentials.connect(name)
2058-
2059- # We do this here because it (1) cleans things up and (2) verifies
2060- # that the account is correctly set up. Both of these are appropriate
2061- # for initialization.
2062- #
2063- # We always recreate the keypairs because there is no way to
2064- # programmatically retrieve the private key component, unless we
2065- # generate it.
2066- account.collect_garbage()
2067-
2068- if machine_id and machine_id.startswith('based-on:'):
2069- from_scratch = True
2070- machine_id = machine_id[len('based-on:'):]
2071- else:
2072- from_scratch = False
2073-
2074- # get the image
2075- image = account.acquire_image(machine_id)
2076-
2077- login = get_lp_login()
2078- if not login:
2079- raise BzrCommandError(
2080- 'you must have set your launchpad login in bzr.')
2081-
2082- instance = EC2Instance(
2083- name, image, instance_type, demo_networks, account,
2084- from_scratch, user_key, login, region)
2085- instance._credentials = credentials
2086- return instance
2087-
2088- def __init__(self, name, image, instance_type, demo_networks, account,
2089- from_scratch, user_key, launchpad_login, region):
2090- self._name = name
2091- self._image = image
2092- self._account = account
2093- self._instance_type = instance_type
2094- self._demo_networks = demo_networks
2095- self._boto_instance = None
2096- self._from_scratch = from_scratch
2097- self._user_key = user_key
2098- self._launchpad_login = launchpad_login
2099- self._region = region
2100-
2101- def log(self, msg):
2102- """Log a message on stdout, flushing afterwards."""
2103- # XXX: JonathanLange 2009-05-31 bug=383076: Should delete this and use
2104- # Python logging module instead.
2105- sys.stdout.write(msg)
2106- sys.stdout.flush()
2107-
2108- def start(self):
2109- """Start the instance."""
2110- if self._boto_instance is not None:
2111- self.log('Instance %s already started' % self._boto_instance.id)
2112- return
2113- start = time.time()
2114- self.private_key = self._account.acquire_private_key()
2115- self.security_group = self._account.acquire_security_group(
2116- demo_networks=self._demo_networks)
2117- reservation = self._image.run(
2118- key_name=self._name, security_groups=[self._name],
2119- instance_type=self._instance_type)
2120- self._boto_instance = reservation.instances[0]
2121- self.log('Instance %s starting..' % self._boto_instance.id)
2122- while self._boto_instance.state == 'pending':
2123- self.log('.')
2124- time.sleep(5)
2125- self._boto_instance.update()
2126- if self._boto_instance.state == 'running':
2127- self.log(' started on %s\n' % self.hostname)
2128- elapsed = time.time() - start
2129- self.log('Started in %d minutes %d seconds\n' %
2130- (elapsed // 60, elapsed % 60))
2131- self._output = self._boto_instance.get_console_output()
2132- self.log(self._output.output)
2133- self._ec2test_user_has_keys = False
2134- else:
2135- raise BzrCommandError(
2136- "failed to start: %s: %r\n" % (
2137- self._boto_instance.state,
2138- self._boto_instance.state_reason,
2139- ))
2140-
2141- def shutdown(self):
2142- """Shut down the instance."""
2143- if self._boto_instance is None:
2144- self.log('no instance created\n')
2145- return
2146- self._boto_instance.update()
2147- if self._boto_instance.state not in ('shutting-down', 'terminated'):
2148- self.log("terminating %s..." % self._boto_instance)
2149- self._boto_instance.terminate()
2150- self._boto_instance.update()
2151- self.log(" done\n")
2152- self.log('instance %s\n' % (self._boto_instance.state,))
2153-
2154- @property
2155- def hostname(self):
2156- if self._boto_instance is None:
2157- return None
2158- return self._boto_instance.public_dns_name
2159-
2160- def _connect(self, username):
2161- """Connect to the instance as `user`. """
2162- ssh = paramiko.SSHClient()
2163- ssh.set_missing_host_key_policy(AcceptAllPolicy())
2164- self.log('ssh connect to %s: ' % self.hostname)
2165- connect_args = {
2166- 'username': username,
2167- 'pkey': self.private_key,
2168- 'allow_agent': False,
2169- 'look_for_keys': False,
2170- }
2171- for count in range(20):
2172- caught_errors = (
2173- socket.error,
2174- paramiko.AuthenticationException,
2175- EOFError,
2176- )
2177- try:
2178- ssh.connect(self.hostname, **connect_args)
2179- except caught_errors as e:
2180- self.log('.')
2181- not_connected = [
2182- errno.ECONNREFUSED,
2183- errno.ETIMEDOUT,
2184- errno.EHOSTUNREACH,
2185- ]
2186- if getattr(e, 'errno', None) not in not_connected:
2187- self.log('ssh _connect: %r\n' % (e,))
2188- if count < 9:
2189- time.sleep(5)
2190- else:
2191- raise
2192- else:
2193- break
2194- self.log(' ok!\n')
2195- return EC2InstanceConnection(self, username, ssh)
2196-
2197- def _upload_local_key(self, conn, remote_filename):
2198- """Upload a key from the local user's agent to `remote_filename`.
2199-
2200- The key will be uploaded in a format suitable for
2201- ~/.ssh/authorized_keys.
2202- """
2203- authorized_keys_file = conn.sftp.open(remote_filename, 'w')
2204- authorized_keys_file.write(
2205- "%s %s\n" % (
2206- self._user_key.get_name(), self._user_key.get_base64()))
2207- authorized_keys_file.close()
2208-
2209- def _ensure_ec2test_user_has_keys(self, connection=None):
2210- """Make sure that we can connect over ssh as the 'ec2test' user.
2211-
2212- We add both the key that was used to start the instance (so
2213- _connect('ec2test') works and a key from the locally running ssh agent
2214- (so EC2InstanceConnection.run_with_ssh_agent works).
2215- """
2216- if not self._ec2test_user_has_keys:
2217- if connection is None:
2218- connection = self._connect('ubuntu')
2219- our_connection = True
2220- else:
2221- our_connection = False
2222- self._upload_local_key(connection, 'local_key')
2223- connection.perform(
2224- 'cat /home/ubuntu/.ssh/authorized_keys local_key '
2225- '| sudo tee /home/ec2test/.ssh/authorized_keys > /dev/null'
2226- '&& rm local_key')
2227- connection.perform('sudo chown -R ec2test:ec2test /home/ec2test/')
2228- connection.perform('sudo chmod 644 /home/ec2test/.ssh/*')
2229- if our_connection:
2230- connection.close()
2231- self.log(
2232- 'You can now use ssh -A ec2test@%s to '
2233- 'log in the instance.\n' % self.hostname)
2234- self._ec2test_user_has_keys = True
2235-
2236- def connect(self):
2237- """Connect to the instance as a user with passwordless sudo.
2238-
2239- This may involve first connecting as root and adding SSH keys to the
2240- user's account, and in the case of a from scratch image, it will do a
2241- lot of set up.
2242- """
2243- if self._from_scratch:
2244- ubuntu_connection = self._connect('ubuntu')
2245- self._upload_local_key(ubuntu_connection, 'local_key')
2246- ubuntu_connection.perform(
2247- 'cat local_key >> ~/.ssh/authorized_keys && rm local_key')
2248- ubuntu_connection.run_script(from_scratch_root, sudo=True)
2249- self._ensure_ec2test_user_has_keys(ubuntu_connection)
2250- ubuntu_connection.close()
2251- conn = self._connect('ec2test')
2252- conn.run_script(
2253- from_scratch_ec2test
2254- % {'launchpad-login': self._launchpad_login})
2255- self._from_scratch = False
2256- self.log('done running from_scratch setup\n')
2257- return conn
2258- self._ensure_ec2test_user_has_keys()
2259- return self._connect('ec2test')
2260-
2261- def _report_traceback(self):
2262- """Print traceback."""
2263- traceback.print_exc()
2264-
2265- def set_up_and_run(self, postmortem, shutdown, func, *args, **kw):
2266- """Start, run `func` and then maybe shut down.
2267-
2268- :param config: A dictionary specifying details of how the instance
2269- should be run:
2270- :param postmortem: If true, any exceptions will be caught and an
2271- interactive session run to allow debugging the problem.
2272- :param shutdown: If true, shut down the instance after `func` and
2273- postmortem (if any) are completed.
2274- :param func: A callable that will be called when the instance is
2275- running and a user account has been set up on it.
2276- :param args: Passed to `func`.
2277- :param kw: Passed to `func`.
2278- """
2279- # We ignore the value of the 'shutdown' argument and always shut down
2280- # unless `func` returns normally.
2281- really_shutdown = True
2282- retval = None
2283- try:
2284- self.start()
2285- try:
2286- retval = func(*args, **kw)
2287- except Exception:
2288- # When running in postmortem mode, it is really helpful to see
2289- # if there are any exceptions before it waits in the console
2290- # (in the finally block), and you can't figure out why it's
2291- # broken.
2292- self._report_traceback()
2293- else:
2294- really_shutdown = shutdown
2295- finally:
2296- try:
2297- if postmortem:
2298- console = code.InteractiveConsole(locals())
2299- console.interact(
2300- postmortem_banner % {'dns': self.hostname})
2301- print 'Postmortem console closed.'
2302- finally:
2303- if really_shutdown:
2304- self.shutdown()
2305- return retval
2306-
2307- def _copy_single_file(self, sftp, local_path, remote_dir):
2308- """Copy `local_path` to `remote_dir` on this instance.
2309-
2310- The name in the remote directory will be that of the local file.
2311-
2312- :param sftp: A paramiko SFTP object.
2313- :param local_path: The local path.
2314- :param remote_dir: The directory on the instance to copy into.
2315- """
2316- name = os.path.basename(local_path)
2317- remote_path = os.path.join(remote_dir, name)
2318- remote_file = sftp.open(remote_path, 'w')
2319- remote_file.write(open(local_path).read())
2320- remote_file.close()
2321- return remote_path
2322-
2323- def copy_key_and_certificate_to_image(self, sftp):
2324- """Copy the AWS private key and certificate to the image.
2325-
2326- :param sftp: A paramiko SFTP object.
2327- """
2328- remote_ec2_dir = '/mnt/ec2'
2329- remote_pk = self._copy_single_file(
2330- sftp, self.local_pk, remote_ec2_dir)
2331- remote_cert = self._copy_single_file(
2332- sftp, self.local_cert, remote_ec2_dir)
2333- return (remote_pk, remote_cert)
2334-
2335- def _check_single_glob_match(self, local_dir, pattern, file_kind):
2336- """Check that `pattern` matches one file in `local_dir` and return it.
2337-
2338- :param local_dir: The local directory to look in.
2339- :param pattern: The glob patten to match.
2340- :param file_kind: The sort of file we're looking for, to be used in
2341- error messages.
2342- """
2343- pattern = os.path.join(local_dir, pattern)
2344- matches = glob.glob(pattern)
2345- if len(matches) != 1:
2346- raise BzrCommandError(
2347- '%r must match a single %s file' % (pattern, file_kind))
2348- return matches[0]
2349-
2350- def check_bundling_prerequisites(self, name):
2351- """Check, as best we can, that all the files we need to bundle exist.
2352- """
2353- local_ec2_dir = os.path.expanduser('~/.ec2')
2354- if not os.path.exists(local_ec2_dir):
2355- raise BzrCommandError(
2356- "~/.ec2 must exist and contain aws_user, aws_id, a private "
2357- "key file and a certificate.")
2358- aws_user_file = os.path.expanduser('~/.ec2/aws_user')
2359- if not os.path.exists(aws_user_file):
2360- raise BzrCommandError(
2361- "~/.ec2/aws_user must exist and contain your numeric AWS id.")
2362- self.aws_user = open(aws_user_file).read().strip()
2363- self.local_cert = self._check_single_glob_match(
2364- local_ec2_dir, 'cert-*.pem', 'certificate')
2365- self.local_pk = self._check_single_glob_match(
2366- local_ec2_dir, 'pk-*.pem', 'private key')
2367- # The bucket `name` needs to exist and be accessible. We create it
2368- # here to reserve the name. If the bucket already exists and conforms
2369- # to the above requirements, this is a no-op.
2370- #
2371- # The API for region creation is a little quirky: you apparently can't
2372- # explicitly ask for 'us-east-1' you must just say '', etc.
2373- location = self._credentials.region_name
2374- if location.startswith('us-east'):
2375- location = ''
2376- elif location.startswith('eu'):
2377- location = 'EU'
2378- self._credentials.connect_s3().create_bucket(
2379- name, location=location)
2380-
2381- def bundle(self, name, credentials):
2382- """Bundle, upload and register the instance as a new AMI.
2383-
2384- :param name: The name-to-be of the new AMI, eg 'launchpad-ec2test500'.
2385- :param credentials: An `EC2Credentials` object.
2386- """
2387- connection = self.connect()
2388- # See http://is.gd/g1MIT . When we switch to Eucalyptus, we can dump
2389- # this installation of the ec2-ami-tools.
2390- connection.perform(
2391- 'sudo env DEBIAN_FRONTEND=noninteractive '
2392- 'apt-get -y install ec2-ami-tools')
2393- connection.perform('rm -f .ssh/authorized_keys')
2394- connection.perform('sudo mkdir /mnt/ec2')
2395- connection.perform('sudo chown $USER:$USER /mnt/ec2')
2396-
2397- remote_pk, remote_cert = self.copy_key_and_certificate_to_image(
2398- connection.sftp)
2399-
2400- bundle_dir = os.path.join('/mnt', name)
2401-
2402- connection.perform('sudo mkdir ' + bundle_dir)
2403- connection.perform(' '.join([
2404- 'sudo ec2-bundle-vol',
2405- '-d %s' % bundle_dir,
2406- '--batch', # Set batch-mode, which doesn't use prompts.
2407- '-k %s' % remote_pk,
2408- '-c %s' % remote_cert,
2409- '-u %s' % self.aws_user,
2410- ]))
2411-
2412- # Assume that the manifest is 'image.manifest.xml', since "image" is
2413- # the default prefix.
2414- manifest = os.path.join(bundle_dir, 'image.manifest.xml')
2415-
2416- # Best check that the manifest actually exists though.
2417- test = 'test -f %s' % manifest
2418- connection.perform(test)
2419-
2420- connection.perform(' '.join([
2421- 'sudo ec2-upload-bundle',
2422- '-b %s' % name,
2423- '-m %s' % manifest,
2424- '-a %s' % credentials.identifier,
2425- '-s %s' % credentials.secret,
2426- ]))
2427-
2428- connection.close()
2429-
2430- # This is invoked locally.
2431- mfilename = os.path.basename(manifest)
2432- manifest_path = os.path.join(name, mfilename)
2433-
2434- now = datetime.strftime(datetime.utcnow(), "%Y-%m-%d %H:%M:%S UTC")
2435- description = "launchpad ec2test created %s by %r on %s" % (
2436- now,
2437- os.environ.get('EMAIL', '<unknown>'),
2438- socket.gethostname())
2439-
2440- self.log('registering image: ')
2441- image_id = credentials.connect('bundle').conn.register_image(
2442- name=name,
2443- description=description,
2444- image_location=manifest_path,
2445- )
2446- self.log('ok\n')
2447- self.log('** new instance: %r\n' % (image_id,))
2448-
2449-
2450-class EC2InstanceConnection:
2451- """An ssh connection to an `EC2Instance`."""
2452-
2453- def __init__(self, instance, username, ssh):
2454- self._instance = instance
2455- self._username = username
2456- self._ssh = ssh
2457- self._sftp = None
2458-
2459- @property
2460- def sftp(self):
2461- if self._sftp is None:
2462- self._sftp = self._ssh.open_sftp()
2463- if self._sftp is None:
2464- raise AssertionError("failed to open sftp connection")
2465- return self._sftp
2466-
2467- def perform(self, cmd, ignore_failure=False, out=None, err=None):
2468- """Perform 'cmd' on server.
2469-
2470- :param ignore_failure: If False, raise an error on non-zero exit
2471- statuses.
2472- :param out: A stream to write the output of the remote command to.
2473- :param err: A stream to write the error of the remote command to.
2474- """
2475- if out is None:
2476- out = sys.stdout
2477- if err is None:
2478- err = sys.stderr
2479- self._instance.log(
2480- '%s@%s$ %s\n'
2481- % (self._username, self._instance._boto_instance.id, cmd))
2482- session = self._ssh.get_transport().open_session()
2483- session.exec_command(cmd)
2484- session.shutdown_write()
2485- while 1:
2486- try:
2487- select.select([session], [], [], 0.5)
2488- except (IOError, select.error), e:
2489- if e.errno == errno.EINTR:
2490- continue
2491- if session.recv_ready():
2492- data = session.recv(4096)
2493- if data:
2494- out.write(data)
2495- out.flush()
2496- if session.recv_stderr_ready():
2497- data = session.recv_stderr(4096)
2498- if data:
2499- err.write(data)
2500- err.flush()
2501- if session.exit_status_ready():
2502- break
2503- session.close()
2504- # XXX: JonathanLange 2009-05-31: If the command is killed by a signal
2505- # on the remote server, the SSH protocol does not send an exit_status,
2506- # it instead sends a different message with the number of the signal
2507- # that killed the process. AIUI, this code will fail confusingly if
2508- # that happens.
2509- res = session.recv_exit_status()
2510- if res and not ignore_failure:
2511- raise RuntimeError('Command failed: %s' % (cmd,))
2512- return res
2513-
2514- def run_with_ssh_agent(self, cmd, ignore_failure=False):
2515- """Run 'cmd' in a subprocess.
2516-
2517- Use this to run commands that require local SSH credentials. For
2518- example, getting private branches from Launchpad.
2519- """
2520- self._instance.log(
2521- '%s@%s$ %s\n'
2522- % (self._username, self._instance._boto_instance.id, cmd))
2523- call = ['ssh', '-A', self._username + '@' + self._instance.hostname,
2524- '-o', 'CheckHostIP no',
2525- '-o', 'StrictHostKeyChecking no',
2526- '-o', 'UserKnownHostsFile ~/.ec2/known_hosts',
2527- cmd]
2528- res = subprocess.call(call)
2529- if res and not ignore_failure:
2530- raise RuntimeError('Command failed: %s' % (cmd,))
2531- return res
2532-
2533- def run_script(self, script_text, sudo=False):
2534- """Upload `script_text` to the instance and run it with bash."""
2535- script = self.sftp.open('script.sh', 'w')
2536- script.write(script_text)
2537- script.close()
2538- cmd = '/bin/bash script.sh'
2539- if sudo:
2540- cmd = 'sudo ' + cmd
2541- self.run_with_ssh_agent(cmd)
2542- # At least for mwhudson, the paramiko connection often drops while the
2543- # script is running. Reconnect just in case.
2544- self.reconnect()
2545- self.perform('rm script.sh')
2546-
2547- def reconnect(self):
2548- """Close the connection and reopen it."""
2549- self.close()
2550- self._ssh = self._instance._connect(self._username)._ssh
2551-
2552- def close(self):
2553- if self._sftp is not None:
2554- self._sftp.close()
2555- self._sftp = None
2556- self._ssh.close()
2557- self._ssh = None
2558
2559=== removed file 'lib/devscripts/ec2test/remote.py'
2560--- lib/devscripts/ec2test/remote.py 2012-01-01 03:03:28 +0000
2561+++ lib/devscripts/ec2test/remote.py 1970-01-01 00:00:00 +0000
2562@@ -1,930 +0,0 @@
2563-#!/usr/bin/env python
2564-# Copyright 2009 Canonical Ltd. This software is licensed under the
2565-# GNU Affero General Public License version 3 (see the file LICENSE).
2566-
2567-"""Run tests in a daemon.
2568-
2569- * `EC2Runner` handles the daemonization and instance shutdown.
2570-
2571- * `Request` knows everything about the test request we're handling (e.g.
2572- "test merging foo-bar-bug-12345 into db-devel").
2573-
2574- * `LaunchpadTester` knows how to actually run the tests and gather the
2575- results. It uses `SummaryResult` to do so.
2576-
2577- * `WebTestLogger` knows how to display the results to the user, and is given
2578- the responsibility of handling the results that `LaunchpadTester` gathers.
2579-"""
2580-
2581-__metatype__ = type
2582-
2583-import datetime
2584-from email import (
2585- MIMEMultipart,
2586- MIMEText,
2587- )
2588-from email.mime.application import MIMEApplication
2589-import errno
2590-import gzip
2591-import optparse
2592-import os
2593-import pickle
2594-from StringIO import StringIO
2595-import subprocess
2596-import sys
2597-import tempfile
2598-import textwrap
2599-import time
2600-import traceback
2601-import unittest
2602-from xml.sax.saxutils import escape
2603-
2604-import bzrlib.branch
2605-import bzrlib.config
2606-from bzrlib.email_message import EmailMessage
2607-import bzrlib.errors
2608-from bzrlib.smtp_connection import SMTPConnection
2609-import bzrlib.workingtree
2610-import simplejson
2611-import subunit
2612-from testtools import MultiTestResult
2613-
2614-# We need to be able to unpickle objects from bzr-pqm, so make sure we
2615-# can import it.
2616-bzrlib.plugin.load_plugins()
2617-
2618-
2619-class NonZeroExitCode(Exception):
2620- """Raised when the child process exits with a non-zero exit code."""
2621-
2622- def __init__(self, retcode):
2623- super(NonZeroExitCode, self).__init__(
2624- 'Test process died with exit code %r, but no tests failed.'
2625- % (retcode,))
2626-
2627-
2628-class SummaryResult(unittest.TestResult):
2629- """Test result object used to generate the summary."""
2630-
2631- double_line = '=' * 70
2632- single_line = '-' * 70
2633-
2634- def __init__(self, output_stream):
2635- super(SummaryResult, self).__init__()
2636- self.stream = output_stream
2637-
2638- def _formatError(self, flavor, test, error):
2639- return '\n'.join(
2640- [self.double_line,
2641- '%s: %s' % (flavor, test),
2642- self.single_line,
2643- error,
2644- ''])
2645-
2646- def addError(self, test, error):
2647- super(SummaryResult, self).addError(test, error)
2648- self.stream.write(
2649- self._formatError(
2650- 'ERROR', test, self._exc_info_to_string(error, test)))
2651-
2652- def addFailure(self, test, error):
2653- super(SummaryResult, self).addFailure(test, error)
2654- self.stream.write(
2655- self._formatError(
2656- 'FAILURE', test, self._exc_info_to_string(error, test)))
2657-
2658- def stopTest(self, test):
2659- super(SummaryResult, self).stopTest(test)
2660- # At the very least, we should be sure that a test's output has been
2661- # completely displayed once it has stopped.
2662- self.stream.flush()
2663-
2664-
2665-class FailureUpdateResult(unittest.TestResult):
2666-
2667- def __init__(self, logger):
2668- super(FailureUpdateResult, self).__init__()
2669- self._logger = logger
2670-
2671- def addError(self, *args, **kwargs):
2672- super(FailureUpdateResult, self).addError(*args, **kwargs)
2673- self._logger.got_failure()
2674-
2675- def addFailure(self, *args, **kwargs):
2676- super(FailureUpdateResult, self).addFailure(*args, **kwargs)
2677- self._logger.got_failure()
2678-
2679-
2680-class EC2Runner:
2681- """Runs generic code in an EC2 instance.
2682-
2683- Handles daemonization, instance shutdown, and email in the case of
2684- catastrophic failure.
2685- """
2686-
2687- # XXX: JonathanLange 2010-08-17: EC2Runner needs tests.
2688-
2689- # The number of seconds we give this script to clean itself up, and for
2690- # 'ec2 test --postmortem' to grab control if needed. If we don't give
2691- # --postmortem enough time to log in via SSH and take control, then this
2692- # server will begin to shutdown on its own.
2693- #
2694- # (FWIW, "grab control" means sending SIGTERM to this script's process id,
2695- # thus preventing fail-safe shutdown.)
2696- SHUTDOWN_DELAY = 60
2697-
2698- def __init__(self, daemonize, pid_filename, shutdown_when_done,
2699- smtp_connection=None, emails=None):
2700- """Make an EC2Runner.
2701-
2702- :param daemonize: Whether or not we will daemonize.
2703- :param pid_filename: The filename to store the pid in.
2704- :param shutdown_when_done: Whether or not to shut down when the tests
2705- are done.
2706- :param smtp_connection: The `SMTPConnection` to use to send email.
2707- :param emails: The email address(es) to send catastrophic failure
2708- messages to. If not provided, the error disappears into the ether.
2709- """
2710- self._should_daemonize = daemonize
2711- self._pid_filename = pid_filename
2712- self._shutdown_when_done = shutdown_when_done
2713- if smtp_connection is None:
2714- config = bzrlib.config.GlobalConfig()
2715- smtp_connection = SMTPConnection(config)
2716- self._smtp_connection = smtp_connection
2717- self._emails = emails
2718- self._daemonized = False
2719-
2720- def _daemonize(self):
2721- """Turn the testrunner into a forked daemon process."""
2722- # This also writes our pidfile to disk to a specific location. The
2723- # ec2test.py --postmortem command will look there to find our PID,
2724- # in order to control this process.
2725- daemonize(self._pid_filename)
2726- self._daemonized = True
2727-
2728- def _shutdown_instance(self):
2729- """Shut down this EC2 instance."""
2730- # Make sure our process is daemonized, and has therefore disconnected
2731- # the controlling terminal. This also disconnects the ec2test.py SSH
2732- # connection, thus signalling ec2test.py that it may now try to take
2733- # control of the server.
2734- if not self._daemonized:
2735- # We only want to do this if we haven't already been daemonized.
2736- # Nesting daemons is bad.
2737- self._daemonize()
2738-
2739- time.sleep(self.SHUTDOWN_DELAY)
2740-
2741- # Cancel the running shutdown.
2742- subprocess.call(['sudo', 'shutdown', '-c'])
2743-
2744- # We'll only get here if --postmortem didn't kill us. This is our
2745- # fail-safe shutdown, in case the user got disconnected or suffered
2746- # some other mishap that would prevent them from shutting down this
2747- # server on their own.
2748- subprocess.call(['sudo', 'shutdown', '-P', 'now'])
2749-
2750- def run(self, name, function, *args, **kwargs):
2751- try:
2752- if self._should_daemonize:
2753- print 'Starting %s daemon...' % (name,)
2754- self._daemonize()
2755-
2756- return function(*args, **kwargs)
2757- except:
2758- config = bzrlib.config.GlobalConfig()
2759- # Handle exceptions thrown by the test() or daemonize() methods.
2760- if self._emails:
2761- msg = EmailMessage(
2762- from_address=config.username(),
2763- to_address=self._emails,
2764- subject='%s FAILED' % (name,),
2765- body=traceback.format_exc())
2766- self._smtp_connection.send_email(msg)
2767- raise
2768- finally:
2769- # When everything is over, if we've been ask to shut down, then
2770- # make sure we're daemonized, then shutdown. Otherwise, if we're
2771- # daemonized, just clean up the pidfile.
2772- if self._shutdown_when_done:
2773- self._shutdown_instance()
2774- elif self._daemonized:
2775- # It would be nice to clean up after ourselves, since we won't
2776- # be shutting down.
2777- remove_pidfile(self._pid_filename)
2778- else:
2779- # We're not a daemon, and we're not shutting down. The user
2780- # most likely started this script manually, from a shell
2781- # running on the instance itself.
2782- pass
2783-
2784-
2785-class LaunchpadTester:
2786- """Runs Launchpad tests and gathers their results in a useful way."""
2787-
2788- def __init__(self, logger, test_directory, test_options=()):
2789- """Construct a TestOnMergeRunner.
2790-
2791- :param logger: The WebTestLogger to log to.
2792- :param test_directory: The directory to run the tests in. We expect
2793- this directory to have a fully-functional checkout of Launchpad
2794- and its dependent branches.
2795- :param test_options: A sequence of options to pass to the test runner.
2796- """
2797- self._logger = logger
2798- self._test_directory = test_directory
2799- self._test_options = ' '.join(test_options)
2800-
2801- def build_test_command(self):
2802- """Return the command that will execute the test suite.
2803-
2804- Should return a list of command options suitable for submission to
2805- subprocess.call()
2806-
2807- Subclasses must provide their own implementation of this method.
2808- """
2809- command = ['make', 'check']
2810- if self._test_options:
2811- command.append('TESTOPTS="%s"' % self._test_options)
2812- return command
2813-
2814- def _spawn_test_process(self):
2815- """Actually run the tests.
2816-
2817- :return: A `subprocess.Popen` object for the test run.
2818- """
2819- call = self.build_test_command()
2820- self._logger.write_line("Running %s" % (call,))
2821- # bufsize=0 means do not buffer any of the output. We want to
2822- # display the test output as soon as it is generated.
2823- return subprocess.Popen(
2824- call, bufsize=0,
2825- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
2826- cwd=self._test_directory)
2827-
2828- def test(self):
2829- """Run the tests, log the results.
2830-
2831- Signals the ec2test process and cleans up the logs once all the tests
2832- have completed. If necessary, submits the branch to PQM, and mails
2833- the user the test results.
2834- """
2835- self._logger.prepare()
2836- try:
2837- popen = self._spawn_test_process()
2838- result = self._gather_test_output(popen.stdout, self._logger)
2839- retcode = popen.wait()
2840- # The process could have an error not indicated by an actual test
2841- # result nor by a raised exception
2842- if result.wasSuccessful() and retcode:
2843- raise NonZeroExitCode(retcode)
2844- except:
2845- self._logger.error_in_testrunner(sys.exc_info())
2846- else:
2847- self._logger.got_result(result)
2848-
2849- def _gather_test_output(self, input_stream, logger):
2850- """Write the testrunner output to the logs."""
2851- summary_stream = logger.get_summary_stream()
2852- summary_result = SummaryResult(summary_stream)
2853- result = MultiTestResult(
2854- summary_result,
2855- FailureUpdateResult(logger))
2856- subunit_server = subunit.TestProtocolServer(result, summary_stream)
2857- for line in input_stream:
2858- subunit_server.lineReceived(line)
2859- logger.got_line(line)
2860- summary_stream.flush()
2861- return summary_result
2862-
2863-
2864-# XXX: Publish a JSON file that includes the relevant details from this
2865-# request.
2866-class Request:
2867- """A request to have a branch tested and maybe landed."""
2868-
2869- def __init__(self, branch_url, revno, local_branch_path, sourcecode_path,
2870- emails=None, pqm_message=None, smtp_connection=None):
2871- """Construct a `Request`.
2872-
2873- :param branch_url: The public URL to the Launchpad branch we are
2874- testing.
2875- :param revno: The revision number of the branch we are testing.
2876- :param local_branch_path: A local path to the Launchpad branch we are
2877- testing. This must be a branch of Launchpad with a working tree.
2878- :param sourcecode_path: A local path to the sourcecode dependencies
2879- directory (normally '$local_branch_path/sourcecode'). This must
2880- contain up-to-date copies of all of Launchpad's sourcecode
2881- dependencies.
2882- :param emails: A list of emails to send the results to. If not
2883- provided, no emails are sent.
2884- :param pqm_message: The message to submit to PQM. If not provided, we
2885- don't submit to PQM.
2886- :param smtp_connection: The `SMTPConnection` to use to send email.
2887- """
2888- self._branch_url = branch_url
2889- self._revno = revno
2890- self._local_branch_path = local_branch_path
2891- self._sourcecode_path = sourcecode_path
2892- self._emails = emails
2893- self._pqm_message = pqm_message
2894- # Used for figuring out how to send emails.
2895- self._bzr_config = bzrlib.config.GlobalConfig()
2896- if smtp_connection is None:
2897- smtp_connection = SMTPConnection(self._bzr_config)
2898- self._smtp_connection = smtp_connection
2899-
2900- def _send_email(self, message):
2901- """Actually send 'message'."""
2902- self._smtp_connection.send_email(message)
2903-
2904- def _format_test_list(self, header, tests):
2905- if not tests:
2906- return []
2907- tests = [' ' + test.id() for test, error in tests]
2908- return [header, '-' * len(header)] + tests + ['']
2909-
2910- def format_result(self, result, start_time, end_time):
2911- duration = end_time - start_time
2912- output = [
2913- 'Tests started at approximately %s' % start_time,
2914- ]
2915- source = self.get_source_details()
2916- if source:
2917- output.append('Source: %s r%s' % source)
2918- target = self.get_target_details()
2919- if target:
2920- output.append('Target: %s r%s' % target)
2921- output.extend([
2922- '',
2923- '%s tests run in %s, %s failures, %s errors' % (
2924- result.testsRun, duration, len(result.failures),
2925- len(result.errors)),
2926- '',
2927- ])
2928-
2929- bad_tests = (
2930- self._format_test_list('Failing tests', result.failures) +
2931- self._format_test_list('Tests with errors', result.errors))
2932- output.extend(bad_tests)
2933-
2934- if bad_tests:
2935- full_error_stream = StringIO()
2936- copy_result = SummaryResult(full_error_stream)
2937- for test, error in result.failures:
2938- full_error_stream.write(
2939- copy_result._formatError('FAILURE', test, error))
2940- for test, error in result.errors:
2941- full_error_stream.write(
2942- copy_result._formatError('ERROR', test, error))
2943- output.append(full_error_stream.getvalue())
2944-
2945- subject = self._get_pqm_subject()
2946- if subject:
2947- if result.wasSuccessful():
2948- output.append('SUBMITTED TO PQM:')
2949- else:
2950- output.append('**NOT** submitted to PQM:')
2951- output.extend([subject, ''])
2952- output.extend(['(See the attached file for the complete log)', ''])
2953- return '\n'.join(output)
2954-
2955- def get_target_details(self):
2956- """Return (branch_url, revno) for trunk."""
2957- branch = bzrlib.branch.Branch.open(self._local_branch_path)
2958- return branch.get_parent().encode('utf-8'), branch.revno()
2959-
2960- def get_source_details(self):
2961- """Return (branch_url, revno) for the branch we're merging in.
2962-
2963- If we're not merging in a branch, but instead just testing a trunk,
2964- then return None.
2965- """
2966- tree = bzrlib.workingtree.WorkingTree.open(self._local_branch_path)
2967- parent_ids = tree.get_parent_ids()
2968- if len(parent_ids) < 2:
2969- return None
2970- return self._branch_url.encode('utf-8'), self._revno
2971-
2972- def _last_segment(self, url):
2973- """Return the last segment of a URL."""
2974- return url.strip('/').split('/')[-1]
2975-
2976- def get_nick(self):
2977- """Get the nick of the branch we are testing."""
2978- details = self.get_source_details()
2979- if not details:
2980- details = self.get_target_details()
2981- url, revno = details
2982- return self._last_segment(url)
2983-
2984- def get_revno(self):
2985- """Get the revno of the branch we are testing."""
2986- if self._revno is not None:
2987- return self._revno
2988- return bzrlib.branch.Branch.open(self._local_branch_path).revno()
2989-
2990- def get_merge_description(self):
2991- """Get a description of the merge request.
2992-
2993- If we're merging a branch, return '$SOURCE_NICK => $TARGET_NICK', if
2994- we're just running tests for a trunk branch without merging return
2995- '$TRUNK_NICK'.
2996- """
2997- source = self.get_source_details()
2998- if not source:
2999- return '%s r%s' % (self.get_nick(), self.get_revno())
3000- target = self.get_target_details()
3001- return '%s => %s' % (
3002- self._last_segment(source[0]), self._last_segment(target[0]))
3003-
3004- def get_summary_commit(self):
3005- """Get a message summarizing the change from the commit log.
3006-
3007- Returns the last commit message of the merged branch, or None.
3008- """
3009- # XXX: JonathanLange 2010-08-17: I don't actually know why we are
3010- # using this commit message as a summary message. It's used in the
3011- # test logs and the EC2 hosted web page.
3012- branch = bzrlib.branch.Branch.open(self._local_branch_path)
3013- tree = bzrlib.workingtree.WorkingTree.open(self._local_branch_path)
3014- parent_ids = tree.get_parent_ids()
3015- if len(parent_ids) == 1:
3016- return None
3017- summary = (
3018- branch.repository.get_revision(parent_ids[1]).get_summary())
3019- return summary.encode('utf-8')
3020-
3021- def _build_report_email(self, successful, body_text, full_log_gz):
3022- """Build a MIME email summarizing the test results.
3023-
3024- :param successful: True for pass, False for failure.
3025- :param body_text: The body of the email to send to the requesters.
3026- :param full_log_gz: A gzip of the full log.
3027- """
3028- message = MIMEMultipart.MIMEMultipart()
3029- message['To'] = ', '.join(self._emails)
3030- message['From'] = self._bzr_config.username()
3031- if successful:
3032- status = 'SUCCESS'
3033- else:
3034- status = 'FAILURE'
3035- subject = 'Test results: %s: %s' % (
3036- self.get_merge_description(), status)
3037- message['Subject'] = subject
3038-
3039- # Make the body.
3040- body = MIMEText.MIMEText(body_text, 'plain', 'utf8')
3041- body['Content-Disposition'] = 'inline'
3042- message.attach(body)
3043-
3044- # Attach the gzipped log.
3045- zipped_log = MIMEApplication(full_log_gz, 'x-gzip')
3046- zipped_log.add_header(
3047- 'Content-Disposition', 'attachment',
3048- filename='%s-r%s.subunit.gz' % (
3049- self.get_nick(), self.get_revno()))
3050- message.attach(zipped_log)
3051- return message
3052-
3053- def send_report_email(self, successful, body_text, full_log_gz):
3054- """Send an email summarizing the test results.
3055-
3056- :param successful: True for pass, False for failure.
3057- :param body_text: The body of the email to send to the requesters.
3058- :param full_log_gz: A gzip of the full log.
3059- """
3060- message = self._build_report_email(successful, body_text, full_log_gz)
3061- self._send_email(message)
3062-
3063- def iter_dependency_branches(self):
3064- """Iterate through the Bazaar branches we depend on."""
3065- for name in sorted(os.listdir(self._sourcecode_path)):
3066- path = os.path.join(self._sourcecode_path, name)
3067- if os.path.isdir(path):
3068- try:
3069- branch = bzrlib.branch.Branch.open(path)
3070- except bzrlib.errors.NotBranchError:
3071- continue
3072- yield name, branch.get_parent(), branch.revno()
3073-
3074- def _get_pqm_subject(self):
3075- if not self._pqm_message:
3076- return
3077- return self._pqm_message.get('Subject')
3078-
3079- def submit_to_pqm(self, successful):
3080- """Submit this request to PQM, if successful & configured to do so."""
3081- subject = self._get_pqm_subject()
3082- if subject and successful:
3083- self._send_email(self._pqm_message)
3084- return subject
3085-
3086- @property
3087- def wants_email(self):
3088- """Do the requesters want emails sent to them?"""
3089- return bool(self._emails)
3090-
3091-
3092-class WebTestLogger:
3093- """Logs test output to disk and a simple web page.
3094-
3095- :ivar successful: Whether the logger has received only successful input up
3096- until now.
3097- """
3098-
3099- def __init__(self, full_log_filename, summary_filename, index_filename,
3100- request, echo_to_stdout):
3101- """Construct a WebTestLogger.
3102-
3103- Because this writes an HTML file with links to the summary and full
3104- logs, you should construct this object with
3105- `WebTestLogger.make_in_directory`, which guarantees that the files
3106- are available in the correct locations.
3107-
3108- :param full_log_filename: Path to a file that will have the full
3109- log output written to it. The file will be overwritten.
3110- :param summary_file: Path to a file that will have a human-readable
3111- summary written to it. The file will be overwritten.
3112- :param index_file: Path to a file that will have an HTML page
3113- written to it. The file will be overwritten.
3114- :param request: A `Request` object representing the thing that's being
3115- tested.
3116- :param echo_to_stdout: Whether or not we should echo output to stdout.
3117- """
3118- self._full_log_filename = full_log_filename
3119- self._summary_filename = summary_filename
3120- self._index_filename = index_filename
3121- self._info_json = os.path.join(
3122- os.path.dirname(index_filename), 'info.json')
3123- self._request = request
3124- self._echo_to_stdout = echo_to_stdout
3125- # Actually set by prepare(), but setting to a dummy value to make
3126- # testing easier.
3127- self._start_time = datetime.datetime.utcnow()
3128- self.successful = True
3129-
3130- @classmethod
3131- def make_in_directory(cls, www_dir, request, echo_to_stdout):
3132- """Make a logger that logs to specific files in `www_dir`.
3133-
3134- :param www_dir: The directory in which to log the files:
3135- current_test.log, summary.log and index.html. These files
3136- will be overwritten.
3137- :param request: A `Request` object representing the thing that's being
3138- tested.
3139- :param echo_to_stdout: Whether or not we should echo output to stdout.
3140- """
3141- files = [
3142- os.path.join(www_dir, 'current_test.log'),
3143- os.path.join(www_dir, 'summary.log'),
3144- os.path.join(www_dir, 'index.html')]
3145- files.extend([request, echo_to_stdout])
3146- return cls(*files)
3147-
3148- def error_in_testrunner(self, exc_info):
3149- """Called when there is a catastrophic error in the test runner."""
3150- exc_type, exc_value, exc_tb = exc_info
3151- # XXX: JonathanLange 2010-08-17: This should probably log to the full
3152- # log as well.
3153- summary = self.get_summary_stream()
3154- summary.write('\n\nERROR IN TESTRUNNER\n\n')
3155- traceback.print_exception(exc_type, exc_value, exc_tb, file=summary)
3156- summary.flush()
3157- if self._request.wants_email:
3158- self._write_to_filename(
3159- self._summary_filename,
3160- '\n(See the attached file for the complete log)\n')
3161- summary = self.get_summary_contents()
3162- full_log_gz = gzip_data(self.get_full_log_contents())
3163- self._request.send_report_email(False, summary, full_log_gz)
3164-
3165- def get_index_contents(self):
3166- """Return the contents of the index.html page."""
3167- return self._get_contents(self._index_filename)
3168-
3169- def get_full_log_contents(self):
3170- """Return the contents of the complete log."""
3171- return self._get_contents(self._full_log_filename)
3172-
3173- def get_summary_contents(self):
3174- """Return the contents of the summary log."""
3175- return self._get_contents(self._summary_filename)
3176-
3177- def get_summary_stream(self):
3178- """Return a stream that, when written to, writes to the summary."""
3179- return open(self._summary_filename, 'a')
3180-
3181- def got_line(self, line):
3182- """Called when we get a line of output from our child processes."""
3183- self._write_to_filename(self._full_log_filename, line)
3184- if self._echo_to_stdout:
3185- sys.stdout.write(line)
3186- sys.stdout.flush()
3187-
3188- def _get_contents(self, filename):
3189- """Get the full contents of 'filename'."""
3190- try:
3191- return open(filename, 'r').read()
3192- except IOError, e:
3193- if e.errno == errno.ENOENT:
3194- return ''
3195-
3196- def got_failure(self):
3197- """Called when we receive word that a test has failed."""
3198- self.successful = False
3199- self._dump_json()
3200-
3201- def got_result(self, result):
3202- """The tests are done and the results are known."""
3203- self._end_time = datetime.datetime.utcnow()
3204- successful = result.wasSuccessful()
3205- self._handle_pqm_submission(successful)
3206- if self._request.wants_email:
3207- email_text = self._request.format_result(
3208- result, self._start_time, self._end_time)
3209- full_log_gz = gzip_data(self.get_full_log_contents())
3210- self._request.send_report_email(successful, email_text, full_log_gz)
3211-
3212- def _handle_pqm_submission(self, successful):
3213- subject = self._request.submit_to_pqm(successful)
3214- if not subject:
3215- return
3216- self.write_line('')
3217- self.write_line('')
3218- if successful:
3219- self.write_line('SUBMITTED TO PQM:')
3220- else:
3221- self.write_line('**NOT** submitted to PQM:')
3222- self.write_line(subject)
3223-
3224- def _write_to_filename(self, filename, msg):
3225- fd = open(filename, 'a')
3226- fd.write(msg)
3227- fd.flush()
3228- fd.close()
3229-
3230- def _write(self, msg):
3231- """Write to the summary and full log file."""
3232- self._write_to_filename(self._full_log_filename, msg)
3233- self._write_to_filename(self._summary_filename, msg)
3234-
3235- def write_line(self, msg):
3236- """Write to the summary and full log file with a newline."""
3237- self._write(msg + '\n')
3238-
3239- def _dump_json(self):
3240- fd = open(self._info_json, 'w')
3241- simplejson.dump(
3242- {'description': self._request.get_merge_description(),
3243- 'failed-yet': not self.successful,
3244- }, fd)
3245- fd.close()
3246-
3247- def prepare(self):
3248- """Prepares the log files on disk.
3249-
3250- Writes three log files: the raw output log, the filtered "summary"
3251- log file, and a HTML index page summarizing the test run paramters.
3252- """
3253- self._dump_json()
3254- # XXX: JonathanLange 2010-07-18: Mostly untested.
3255- log = self.write_line
3256-
3257- # Clear the existing index file.
3258- index = open(self._index_filename, 'w')
3259- index.truncate(0)
3260- index.close()
3261-
3262- def add_to_html(html):
3263- return self._write_to_filename(
3264- self._index_filename, textwrap.dedent(html))
3265-
3266- self._start_time = datetime.datetime.utcnow()
3267- msg = 'Tests started at approximately %(now)s UTC' % {
3268- 'now': self._start_time.strftime('%a, %d %b %Y %H:%M:%S')}
3269- add_to_html('''\
3270- <html>
3271- <head>
3272- <title>Testing</title>
3273- </head>
3274- <body>
3275- <h1>Testing</h1>
3276- <p>%s</p>
3277- <ul>
3278- <li><a href="summary.log">Summary results</a></li>
3279- <li><a href="current_test.log">Full results</a></li>
3280- </ul>
3281- ''' % (msg,))
3282- log(msg)
3283-
3284- add_to_html('''\
3285- <h2>Branches Tested</h2>
3286- ''')
3287-
3288- # Describe the trunk branch.
3289- trunk, trunk_revno = self._request.get_target_details()
3290- msg = '%s, revision %d\n' % (trunk, trunk_revno)
3291- add_to_html('''\
3292- <p><strong>%s</strong></p>
3293- ''' % (escape(msg),))
3294- log(msg)
3295-
3296- branch_details = self._request.get_source_details()
3297- if not branch_details:
3298- add_to_html('<p>(no merged branch)</p>\n')
3299- log('(no merged branch)')
3300- else:
3301- branch_name, branch_revno = branch_details
3302- data = {'name': branch_name,
3303- 'revno': branch_revno,
3304- 'commit': self._request.get_summary_commit()}
3305- msg = ('%(name)s, revision %(revno)d '
3306- '(commit message: %(commit)s)\n' % data)
3307- add_to_html('''\
3308- <p>Merged with<br />%(msg)s</p>
3309- ''' % {'msg': escape(msg)})
3310- log("Merged with")
3311- log(msg)
3312-
3313- add_to_html('<dl>\n')
3314- log('\nDEPENDENCY BRANCHES USED\n')
3315- for name, branch, revno in self._request.iter_dependency_branches():
3316- data = {'name': name, 'branch': branch, 'revno': revno}
3317- log(
3318- '- %(name)s\n %(branch)s\n %(revno)d\n' % data)
3319- escaped_data = {'name': escape(name),
3320- 'branch': escape(branch),
3321- 'revno': revno}
3322- add_to_html('''\
3323- <dt>%(name)s</dt>
3324- <dd>%(branch)s</dd>
3325- <dd>%(revno)s</dd>
3326- ''' % escaped_data)
3327- add_to_html('''\
3328- </dl>
3329- </body>
3330- </html>''')
3331- log('\n\nTEST RESULTS FOLLOW\n\n')
3332-
3333-
3334-def daemonize(pid_filename):
3335- # this seems like the sort of thing that ought to be in the
3336- # standard library :-/
3337- pid = os.fork()
3338- if (pid == 0): # Child 1
3339- os.setsid()
3340- pid = os.fork()
3341- if (pid == 0): # Child 2, the daemon.
3342- pass # lookie, we're ready to do work in the daemon
3343- else:
3344- os._exit(0)
3345- else: # Parent
3346- # Make sure the pidfile is written before we exit, so that people
3347- # who've chosen to daemonize can quickly rectify their mistake. Since
3348- # the daemon might terminate itself very, very quickly, we cannot poll
3349- # for the existence of the pidfile. Instead, we just sleep for a
3350- # reasonable amount of time.
3351- time.sleep(1)
3352- os._exit(0)
3353-
3354- # write a pidfile ASAP
3355- write_pidfile(pid_filename)
3356-
3357- # Iterate through and close all file descriptors.
3358- import resource
3359- maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
3360- assert maxfd != resource.RLIM_INFINITY
3361- for fd in range(0, maxfd):
3362- try:
3363- os.close(fd)
3364- except OSError:
3365- # we assume fd was closed
3366- pass
3367- os.open(os.devnull, os.O_RDWR) # this will be 0
3368- os.dup2(0, 1)
3369- os.dup2(0, 2)
3370-
3371-
3372-def gunzip_data(data):
3373- """Decompress 'data'.
3374-
3375- :param data: The gzip data to decompress.
3376- :return: The decompressed data.
3377- """
3378- fd, path = tempfile.mkstemp()
3379- os.write(fd, data)
3380- os.close(fd)
3381- try:
3382- return gzip.open(path, 'r').read()
3383- finally:
3384- os.unlink(path)
3385-
3386-
3387-def gzip_data(data):
3388- """Compress 'data'.
3389-
3390- :param data: The data to compress.
3391- :return: The gzip-compressed data.
3392- """
3393- fd, path = tempfile.mkstemp()
3394- os.close(fd)
3395- gz = gzip.open(path, 'wb')
3396- gz.writelines(data)
3397- gz.close()
3398- try:
3399- return open(path).read()
3400- finally:
3401- os.unlink(path)
3402-
3403-
3404-def write_pidfile(pid_filename):
3405- """Write a pidfile for the current process."""
3406- pid_file = open(pid_filename, "w")
3407- pid_file.write(str(os.getpid()))
3408- pid_file.close()
3409-
3410-
3411-def remove_pidfile(pid_filename):
3412- if os.path.exists(pid_filename):
3413- os.remove(pid_filename)
3414-
3415-
3416-def parse_options(argv):
3417- """Make an `optparse.OptionParser` for running the tests remotely.
3418- """
3419- parser = optparse.OptionParser(
3420- usage="%prog [options] [-- test options]",
3421- description=("Build and run tests for an instance."))
3422- parser.add_option(
3423- '-e', '--email', action='append', dest='email', default=None,
3424- help=('Email address to which results should be mailed. Defaults to '
3425- 'the email address from `bzr whoami`. May be supplied multiple '
3426- 'times. `bzr whoami` will be used as the From: address.'))
3427- parser.add_option(
3428- '-s', '--submit-pqm-message', dest='pqm_message', default=None,
3429- help=('A base64-encoded pickle (string) of a pqm message '
3430- '(bzrib.plugins.pqm.pqm_submit.PQMEmailMessage) to submit if '
3431- 'the test run is successful.'))
3432- parser.add_option(
3433- '--daemon', dest='daemon', default=False,
3434- action='store_true', help=('Run test in background as daemon.'))
3435- parser.add_option(
3436- '--debug', dest='debug', default=False,
3437- action='store_true',
3438- help=('Drop to pdb trace as soon as possible.'))
3439- parser.add_option(
3440- '--shutdown', dest='shutdown', default=False,
3441- action='store_true',
3442- help=('Terminate (shutdown) instance after completion.'))
3443- parser.add_option(
3444- '--public-branch', dest='public_branch', default=None,
3445- help=('The URL of the public branch being tested.'))
3446- parser.add_option(
3447- '--public-branch-revno', dest='public_branch_revno',
3448- type="int", default=None,
3449- help=('The revision number of the public branch being tested.'))
3450-
3451- return parser.parse_args(argv)
3452-
3453-
3454-def main(argv):
3455- options, args = parse_options(argv)
3456-
3457- if options.debug:
3458- import pdb; pdb.set_trace()
3459- if options.pqm_message is not None:
3460- pqm_message = pickle.loads(
3461- options.pqm_message.decode('string-escape').decode('base64'))
3462- else:
3463- pqm_message = None
3464-
3465- # Locations for Launchpad. These are actually defined by the configuration
3466- # of the EC2 image that we use.
3467- LAUNCHPAD_DIR = '/var/launchpad'
3468- TEST_DIR = os.path.join(LAUNCHPAD_DIR, 'test')
3469- SOURCECODE_DIR = os.path.join(TEST_DIR, 'sourcecode')
3470-
3471- pid_filename = os.path.join(LAUNCHPAD_DIR, 'ec2test-remote.pid')
3472-
3473- smtp_connection = SMTPConnection(bzrlib.config.GlobalConfig())
3474-
3475- request = Request(
3476- options.public_branch, options.public_branch_revno, TEST_DIR,
3477- SOURCECODE_DIR, options.email, pqm_message, smtp_connection)
3478- # Only write to stdout if we are running as the foreground process.
3479- echo_to_stdout = not options.daemon
3480- logger = WebTestLogger.make_in_directory(
3481- '/var/www', request, echo_to_stdout)
3482-
3483- runner = EC2Runner(
3484- options.daemon, pid_filename, options.shutdown,
3485- smtp_connection, options.email)
3486-
3487- tester = LaunchpadTester(logger, TEST_DIR, test_options=args[1:])
3488- runner.run("Test runner", tester.test)
3489-
3490-
3491-if __name__ == '__main__':
3492- main(sys.argv)
3493
3494=== removed file 'lib/devscripts/ec2test/session.py'
3495--- lib/devscripts/ec2test/session.py 2012-01-01 03:03:28 +0000
3496+++ lib/devscripts/ec2test/session.py 1970-01-01 00:00:00 +0000
3497@@ -1,96 +0,0 @@
3498-# Copyright 2009 Canonical Ltd. This software is licensed under the
3499-# GNU Affero General Public License version 3 (see the file LICENSE).
3500-
3501-"""Code to represent a single session of EC2 use."""
3502-
3503-__metaclass__ = type
3504-__all__ = [
3505- 'EC2SessionName',
3506- ]
3507-
3508-from datetime import (
3509- datetime,
3510- timedelta,
3511- )
3512-
3513-from devscripts.ec2test.utils import (
3514- find_datetime_string,
3515- make_datetime_string,
3516- make_random_string,
3517- )
3518-
3519-
3520-DEFAULT_LIFETIME = timedelta(hours=6)
3521-
3522-
3523-class EC2SessionName(str):
3524- """A name for an EC2 session.
3525-
3526- This is used when naming key pairs and security groups, so it's
3527- useful to be unique. However, to aid garbage collection of old key
3528- pairs and security groups, the name contains a common element and
3529- an expiry timestamp. The form taken should always be:
3530-
3531- <base-name>/<expires-timestamp>/<random-data>
3532-
3533- None of the parts should contain forward-slashes, and the
3534- timestamp should be acceptable input to `find_datetime_string`.
3535-
3536- `EC2SessionName.make()` will generate a suitable name given a
3537- suitable base name.
3538- """
3539-
3540- @classmethod
3541- def make(cls, base, expires=None):
3542- """Create an `EC2SessionName`.
3543-
3544- This checks that `base` does not contain a forward-slash, and
3545- provides some convenient functionality for `expires`:
3546-
3547- - If `expires` is None, it defaults to now (UTC) plus
3548- `DEFAULT_LIFETIME`.
3549-
3550- - If `expires` is a `datetime`, it is converted to a timestamp
3551- in the correct form.
3552-
3553- - If `expires` is a `timedelta`, it is added to now (UTC) then
3554- converted to a timestamp.
3555-
3556- - Otherwise `expires` is assumed to be a string, so is checked
3557- for the absense of forward-slashes, and that a correctly
3558- formed timestamp can be discovered.
3559-
3560- """
3561- assert '/' not in base
3562- if expires is None:
3563- expires = DEFAULT_LIFETIME
3564- if isinstance(expires, timedelta):
3565- expires = datetime.utcnow() + expires
3566- if isinstance(expires, datetime):
3567- expires = make_datetime_string(expires)
3568- else:
3569- assert '/' not in expires
3570- assert find_datetime_string(expires) is not None
3571- rand = make_random_string(8)
3572- return cls("%s/%s/%s" % (base, expires, rand))
3573-
3574- @property
3575- def base(self):
3576- parts = self.split('/')
3577- if len(parts) != 3:
3578- return None
3579- return parts[0]
3580-
3581- @property
3582- def expires(self):
3583- parts = self.split('/')
3584- if len(parts) != 3:
3585- return None
3586- return find_datetime_string(parts[1])
3587-
3588- @property
3589- def rand(self):
3590- parts = self.split('/')
3591- if len(parts) != 3:
3592- return None
3593- return parts[2]
3594
3595=== removed file 'lib/devscripts/ec2test/testrunner.py'
3596--- lib/devscripts/ec2test/testrunner.py 2012-02-07 14:53:38 +0000
3597+++ lib/devscripts/ec2test/testrunner.py 1970-01-01 00:00:00 +0000
3598@@ -1,548 +0,0 @@
3599-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
3600-# GNU Affero General Public License version 3 (see the file LICENSE).
3601-
3602-"""Code to actually run the tests in an EC2 instance."""
3603-
3604-__metaclass__ = type
3605-__all__ = [
3606- 'EC2TestRunner',
3607- 'TRUNK_BRANCH',
3608- ]
3609-
3610-import os
3611-import pickle
3612-import re
3613-import sys
3614-
3615-from bzrlib.branch import Branch
3616-from bzrlib.bzrdir import BzrDir
3617-from bzrlib.config import GlobalConfig
3618-from bzrlib.errors import UncommittedChanges
3619-from bzrlib.plugins.pqm.pqm_submit import (
3620- NoPQMSubmissionAddress,
3621- PQMSubmission,
3622- )
3623-
3624-
3625-TRUNK_BRANCH = 'bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel'
3626-
3627-
3628-class UnknownBranchURL(Exception):
3629- """Raised when we try to parse an unrecognized branch url."""
3630-
3631- def __init__(self, branch_url):
3632- Exception.__init__(
3633- self,
3634- "Couldn't parse '%s', not a Launchpad branch." % (branch_url,))
3635-
3636-
3637-def parse_branch_url(branch_url):
3638- """Given the URL of a branch, return its components in a dict."""
3639- _lp_match = re.compile(
3640- r'lp:\~([^/]+)/([^/]+)/([^/]+)$').match
3641- _bazaar_match = re.compile(
3642- r'bzr+ssh://bazaar.launchpad.net/\~([^/]+)/([^/]+)/([^/]+)$').match
3643- match = _lp_match(branch_url)
3644- if match is None:
3645- match = _bazaar_match(branch_url)
3646- if match is None:
3647- raise UnknownBranchURL(branch_url)
3648- owner = match.group(1)
3649- product = match.group(2)
3650- branch = match.group(3)
3651- unique_name = '~%s/%s/%s' % (owner, product, branch)
3652- url = 'bzr+ssh://bazaar.launchpad.net/%s' % (unique_name,)
3653- return dict(
3654- owner=owner, product=product, branch=branch, unique_name=unique_name,
3655- url=url)
3656-
3657-
3658-def normalize_branch_input(data):
3659- """Given 'data' return a ('dest', 'src') pair.
3660-
3661- :param data: One of::
3662- - a double of (sourcecode_location, branch_url).
3663- If 'sourcecode_location' is Launchpad, then 'branch_url' can
3664- also be the name of a branch of launchpad owned by
3665- launchpad-pqm.
3666- - a singleton of (branch_url,)
3667- - a singleton of (sourcecode_location,) where
3668- sourcecode_location corresponds to a Launchpad upstream
3669- project as well as a rocketfuel sourcecode location.
3670- - a string which could populate any of the above singletons.
3671-
3672- :return: ('dest', 'src') where 'dest' is the destination
3673- sourcecode location in the rocketfuel tree and 'src' is the
3674- URL of the branch to put there. The URL can be either a bzr+ssh
3675- URL or the name of a branch of launchpad owned by launchpad-pqm.
3676- """
3677- # XXX: JonathanLange 2009-06-05: Should convert lp: URL branches to
3678- # bzr+ssh:// branches.
3679- if isinstance(data, basestring):
3680- data = (data,)
3681- if len(data) == 2:
3682- # Already in dest, src format.
3683- return data
3684- if len(data) != 1:
3685- raise ValueError(
3686- 'invalid argument for ``branches`` argument: %r' %
3687- (data,))
3688- branch_location = data[0]
3689- try:
3690- parsed_url = parse_branch_url(branch_location)
3691- except UnknownBranchURL:
3692- return branch_location, 'lp:%s' % (branch_location,)
3693- return parsed_url['product'], parsed_url['url']
3694-
3695-
3696-def parse_specified_branches(branches):
3697- """Given 'branches' from the command line, return a sanitized dict.
3698-
3699- The dict maps sourcecode locations to branch URLs, according to the
3700- rules in `normalize_branch_input`.
3701- """
3702- return dict(map(normalize_branch_input, branches))
3703-
3704-
3705-class EC2TestRunner:
3706-
3707- name = 'ec2-test-runner'
3708-
3709- message = image = None
3710- _running = False
3711-
3712- def __init__(self, branch, email=False, file=None, test_options=None,
3713- headless=False, branches=(),
3714- pqm_message=None, pqm_public_location=None,
3715- pqm_submit_location=None,
3716- open_browser=False, pqm_email=None,
3717- include_download_cache_changes=None, instance=None,
3718- launchpad_login=None,
3719- timeout=None):
3720- """Create a new EC2TestRunner.
3721-
3722- :param timeout: Number of minutes before we force a shutdown. This is
3723- useful because sometimes the normal instance termination might
3724- fail.
3725-
3726- - original_branch
3727- - test_options
3728- - headless
3729- - include_download_cache_changes
3730- - download_cache_additions
3731- - branches (parses, validates)
3732- - message (after validating PQM submisson)
3733- - email (after validating email capabilities)
3734- - image (after connecting to ec2)
3735- - file
3736- - timeout
3737- """
3738- self.original_branch = branch
3739- self.test_options = test_options
3740- self.headless = headless
3741- self.include_download_cache_changes = include_download_cache_changes
3742- self.open_browser = open_browser
3743- self.file = file
3744- self._launchpad_login = launchpad_login
3745- self.timeout = timeout
3746-
3747- trunk_specified = False
3748- trunk_branch = TRUNK_BRANCH
3749-
3750- # normalize and validate branches
3751- branches = parse_specified_branches(branches)
3752- try:
3753- launchpad_url = branches.pop('launchpad')
3754- except KeyError:
3755- # No Launchpad branch specified.
3756- pass
3757- else:
3758- try:
3759- parsed_url = parse_branch_url(launchpad_url)
3760- except UnknownBranchURL:
3761- user = 'launchpad-pqm'
3762- src = ('bzr+ssh://bazaar.launchpad.net/'
3763- '~launchpad-pqm/launchpad/%s' % (launchpad_url,))
3764- else:
3765- user = parsed_url['owner']
3766- src = parsed_url['url']
3767- if user == 'launchpad-pqm':
3768- trunk_specified = True
3769- trunk_branch = src
3770- self._trunk_branch = trunk_branch
3771- self.branches = branches.items()
3772-
3773- # XXX: JonathanLange 2009-05-31: The trunk_specified stuff above and
3774- # the pqm location stuff below are actually doing the equivalent of
3775- # preparing a merge directive. Perhaps we can leverage that to make
3776- # this code simpler.
3777- self.download_cache_additions = None
3778- if branch is None:
3779- config = GlobalConfig()
3780- if pqm_message is not None:
3781- raise ValueError('Cannot submit trunk to pqm.')
3782- else:
3783- (tree,
3784- bzrbranch,
3785- relpath) = BzrDir.open_containing_tree_or_branch(branch)
3786- config = bzrbranch.get_config()
3787-
3788- if pqm_message is not None or tree is not None:
3789- # if we are going to maybe send a pqm_message, we're going to
3790- # go down this path. Also, even if we are not but this is a
3791- # local branch, we're going to use the PQM machinery to make
3792- # sure that the local branch has been made public, and has all
3793- # working changes there.
3794- if tree is None:
3795- # remote. We will make some assumptions.
3796- if pqm_public_location is None:
3797- pqm_public_location = branch
3798- if pqm_submit_location is None:
3799- pqm_submit_location = trunk_branch
3800- elif pqm_submit_location is None and trunk_specified:
3801- pqm_submit_location = trunk_branch
3802- # Modified from pqm_submit.py.
3803- submission = PQMSubmission(
3804- source_branch=bzrbranch,
3805- public_location=pqm_public_location,
3806- message=pqm_message or '',
3807- submit_location=pqm_submit_location,
3808- tree=tree)
3809- if tree is not None:
3810- # This is the part we want to do whether we're
3811- # submitting or not:
3812- submission.check_tree() # Any working changes.
3813- submission.check_public_branch() # Everything public.
3814- branch = submission.public_location
3815- if (include_download_cache_changes is None or
3816- include_download_cache_changes):
3817- # We need to get the download cache settings.
3818- cache_tree, cache_bzrbranch, cache_relpath = (
3819- BzrDir.open_containing_tree_or_branch(
3820- os.path.join(
3821- self.original_branch, 'download-cache')))
3822- cache_tree.lock_read()
3823- try:
3824- cache_basis_tree = cache_tree.basis_tree()
3825- cache_basis_tree.lock_read()
3826- try:
3827- delta = cache_tree.changes_from(
3828- cache_basis_tree, want_unversioned=True)
3829- unversioned = [
3830- un for un in delta.unversioned
3831- if not cache_tree.is_ignored(un[0])]
3832- added = delta.added
3833- self.download_cache_additions = (
3834- unversioned + added)
3835- finally:
3836- cache_basis_tree.unlock()
3837- finally:
3838- cache_tree.unlock()
3839- if pqm_message is not None:
3840- if self.download_cache_additions:
3841- raise UncommittedChanges(cache_tree)
3842- # Get the submission message.
3843- mail_from = config.get_user_option('pqm_user_email')
3844- if not mail_from:
3845- mail_from = config.username()
3846- mail_from = mail_from.encode('utf8')
3847- if pqm_email is None:
3848- if tree is None:
3849- pqm_email = (
3850- "Launchpad PQM <launchpad@pqm.canonical.com>")
3851- else:
3852- pqm_email = config.get_user_option('pqm_email')
3853- if not pqm_email:
3854- raise NoPQMSubmissionAddress(bzrbranch)
3855- mail_to = pqm_email.encode('utf8')
3856- self.message = submission.to_email(mail_from, mail_to)
3857- elif (self.download_cache_additions and
3858- self.include_download_cache_changes is None):
3859- raise UncommittedChanges(
3860- cache_tree,
3861- 'You must select whether to include download cache '
3862- 'changes (see --include-download-cache-changes and '
3863- '--ignore-download-cache-changes, -c and -g '
3864- 'respectively), or '
3865- 'commit or remove the files in the download-cache.')
3866- self._branch = branch
3867-
3868- if email is not False:
3869- if email is True:
3870- email = [config.username()]
3871- if not email[0]:
3872- raise ValueError('cannot find your email address.')
3873- elif isinstance(email, basestring):
3874- email = [email]
3875- else:
3876- tmp = []
3877- for item in email:
3878- if not isinstance(item, basestring):
3879- raise ValueError(
3880- 'email must be True, False, a string, or a list '
3881- 'of strings')
3882- tmp.append(item)
3883- email = tmp
3884- else:
3885- email = None
3886- self.email = email
3887-
3888- # Email configuration.
3889- if email is not None or pqm_message is not None:
3890- self._smtp_server = config.get_user_option('smtp_server')
3891- # Refuse localhost, because there's no SMTP server _on the actual
3892- # EC2 instance._
3893- if self._smtp_server is None or self._smtp_server == 'localhost':
3894- raise ValueError(
3895- 'To send email, a remotely accessible smtp_server (and '
3896- 'smtp_username and smtp_password, if necessary) must be '
3897- 'configured in bzr. See the SMTP server information '
3898- 'here: https://wiki.canonical.com/EmailSetup .'
3899- 'This server must be reachable from the EC2 instance.')
3900- self._smtp_username = config.get_user_option('smtp_username')
3901- self._smtp_password = config.get_user_option('smtp_password')
3902- self._from_email = config.username()
3903- if not self._from_email:
3904- raise ValueError(
3905- 'To send email, your bzr email address must be set '
3906- '(use ``bzr whoami``).')
3907-
3908- self._instance = instance
3909-
3910- def log(self, msg):
3911- """Log a message on stdout, flushing afterwards."""
3912- # XXX: JonathanLange 2009-05-31 bug=383076: This should use Python
3913- # logging, rather than printing to stdout.
3914- sys.stdout.write(msg)
3915- sys.stdout.flush()
3916-
3917- def configure_system(self):
3918- user_connection = self._instance.connect()
3919- if self.timeout is not None:
3920- # Activate a fail-safe shutdown just in case something goes
3921- # really wrong with the server or suite.
3922- user_connection.perform("sudo shutdown -P +%d &" % self.timeout)
3923- as_user = user_connection.perform
3924- as_user(
3925- "sudo mount "
3926- "-o remount,data=writeback,commit=3600,async,relatime /")
3927- for d in ['/tmp', '/var/tmp']:
3928- as_user(
3929- "sudo mkdir -p %s && sudo mount -t tmpfs none %s" % (d, d))
3930- as_user(
3931- "sudo service postgresql stop"
3932- "; sudo mv /var/lib/postgresql /tmp/postgresql-tmp"
3933- "&& sudo mkdir /var/lib/postgresql"
3934- "&& sudo mount -t tmpfs none /var/lib/postgresql"
3935- "&& sudo mv /tmp/postgresql-tmp/* /var/lib/postgresql"
3936- "&& sudo service postgresql start")
3937- as_user("sudo add-apt-repository ppa:bzr")
3938- as_user("sudo add-apt-repository ppa:launchpad")
3939- as_user("sudo aptitude update")
3940- as_user(
3941- "sudo DEBIAN_FRONTEND=noninteractive aptitude -y full-upgrade")
3942- # Set up bazaar.conf with smtp information if necessary
3943- if self.email or self.message:
3944- as_user('[ -d .bazaar ] || mkdir .bazaar')
3945- bazaar_conf_file = user_connection.sftp.open(
3946- ".bazaar/bazaar.conf", 'w')
3947- bazaar_conf_file.write(
3948- 'email = %s\n' % (self._from_email.encode('utf-8'),))
3949- bazaar_conf_file.write(
3950- 'smtp_server = %s\n' % (self._smtp_server,))
3951- if self._smtp_username:
3952- bazaar_conf_file.write(
3953- 'smtp_username = %s\n' % (self._smtp_username,))
3954- if self._smtp_password:
3955- bazaar_conf_file.write(
3956- 'smtp_password = %s\n' % (self._smtp_password,))
3957- bazaar_conf_file.close()
3958- # Copy remote ec2-remote over
3959- self.log('Copying remote.py to remote machine.\n')
3960- user_connection.sftp.put(
3961- os.path.join(
3962- os.path.dirname(os.path.realpath(__file__)), 'remote.py'),
3963- '/var/launchpad/ec2test-remote.py')
3964- # Set up launchpad login and email
3965- as_user('bzr launchpad-login %s' % (self._launchpad_login,))
3966- user_connection.close()
3967-
3968- def prepare_tests(self):
3969- user_connection = self._instance.connect()
3970- # Clean up the test branch left in the instance image.
3971- user_connection.perform('rm -rf /var/launchpad/test')
3972- user_connection.perform(
3973- 'sudo mkdir /var/launchpad/test && '
3974- 'sudo mount -t tmpfs none /var/launchpad/test')
3975- # Get trunk.
3976- user_connection.run_with_ssh_agent(
3977- 'bzr branch --use-existing-dir %s /var/launchpad/test'
3978- % (self._trunk_branch,))
3979- # Merge the branch in.
3980- if self._branch is not None:
3981- user_connection.run_with_ssh_agent(
3982- 'cd /var/launchpad/test; bzr merge %s' % (self._branch,))
3983- else:
3984- self.log('(Testing trunk, so no branch merge.)')
3985- # get newest sources
3986- user_connection.run_with_ssh_agent(
3987- "/var/launchpad/test/utilities/update-sourcecode "
3988- "/var/launchpad/sourcecode")
3989- # Get any new sourcecode branches as requested
3990- for dest, src in self.branches:
3991- fulldest = os.path.join('/var/launchpad/test/sourcecode', dest)
3992- user_connection.run_with_ssh_agent(
3993- 'bzr branch --standalone %s %s' % (src, fulldest))
3994- # prepare fresh copy of sourcecode and buildout sources for building
3995- p = user_connection.perform
3996- p('rm -rf /var/launchpad/tmp'
3997- '&& mkdir /var/launchpad/tmp '
3998- '&& sudo mount -t tmpfs none /var/launchpad/tmp')
3999- p('mv /var/launchpad/sourcecode /var/launchpad/tmp/sourcecode')
4000- p('mkdir /var/launchpad/tmp/eggs')
4001- p('mkdir /var/launchpad/tmp/yui')
4002- user_connection.run_with_ssh_agent(
4003- 'bzr pull lp:lp-source-dependencies '
4004- '-d /var/launchpad/download-cache')
4005- p(
4006- 'mv /var/launchpad/download-cache '
4007- '/var/launchpad/tmp/download-cache')
4008- if (self.include_download_cache_changes and
4009- self.download_cache_additions):
4010- root = os.path.realpath(
4011- os.path.join(self.original_branch, 'download-cache'))
4012- for info in self.download_cache_additions:
4013- src = os.path.join(root, info[0])
4014- self.log('Copying %s to remote machine.\n' % (src,))
4015- user_connection.sftp.put(
4016- src,
4017- os.path.join(
4018- '/var/launchpad/tmp/download-cache', info[0]))
4019- p('/var/launchpad/test/utilities/link-external-sourcecode '
4020- '-p/var/launchpad/tmp -t/var/launchpad/test'),
4021- # set up database
4022- p('/var/launchpad/test/utilities/launchpad-database-setup $USER')
4023- p('mkdir -p /var/tmp/launchpad_mailqueue/cur')
4024- p('mkdir -p /var/tmp/launchpad_mailqueue/new')
4025- p('mkdir -p /var/tmp/launchpad_mailqueue/tmp')
4026- p('chmod -R a-w /var/tmp/launchpad_mailqueue/')
4027- # close ssh connection
4028- user_connection.close()
4029-
4030- def run_demo_server(self):
4031- """Turn ec2 instance into a demo server."""
4032- self.configure_system()
4033- self.prepare_tests()
4034- user_connection = self._instance.connect()
4035- p = user_connection.perform
4036- p('make -C /var/launchpad/test schema')
4037- p('mkdir -p /var/tmp/bazaar.launchpad.dev/static')
4038- p('mkdir -p /var/tmp/bazaar.launchpad.dev/mirrors')
4039- p('sudo a2enmod proxy > /dev/null')
4040- p('sudo a2enmod proxy_http > /dev/null')
4041- p('sudo a2enmod rewrite > /dev/null')
4042- p('sudo a2enmod ssl > /dev/null')
4043- p('sudo a2enmod deflate > /dev/null')
4044- p('sudo a2enmod headers > /dev/null')
4045- # Install apache config file.
4046- p('cd /var/launchpad/test/; sudo make install')
4047- # Use raw string to eliminate the need to escape the backslash.
4048- # Put eth0's ip address in the /tmp/ip file.
4049- p(r"ifconfig eth0 | grep 'inet addr' "
4050- r"| sed -re 's/.*addr:([0-9.]*) .*/\1/' > /tmp/ip")
4051- # Replace 127.0.0.88 in Launchpad's apache config file with the
4052- # ip address just stored in the /tmp/ip file. Perl allows for
4053- # inplace editing unlike sed.
4054- p('sudo perl -pi -e "s/127.0.0.88/$(cat /tmp/ip)/g" '
4055- '/etc/apache2/sites-available/local-launchpad')
4056- # Restart apache.
4057- p('sudo /etc/init.d/apache2 restart')
4058- # Build mailman and minified javascript, etc.
4059- p('cd /var/launchpad/test/; make')
4060- # Start launchpad in the background.
4061- p('cd /var/launchpad/test/; make start')
4062- # close ssh connection
4063- user_connection.close()
4064-
4065- def _build_command(self):
4066- """Build the command that we'll use to run the tests."""
4067- # Make sure we activate the failsafe --shutdown feature. This will
4068- # make the server shut itself down after the test run completes, or
4069- # if the test harness suffers a critical failure.
4070- cmd = ['python /var/launchpad/ec2test-remote.py --shutdown']
4071-
4072- # Do we want to email the results to the user?
4073- if self.email:
4074- for email in self.email:
4075- cmd.append("--email='%s'" % (
4076- email.encode('utf8').encode('string-escape'),))
4077-
4078- # Do we want to submit the branch to PQM if the tests pass?
4079- if self.message is not None:
4080- cmd.append(
4081- "--submit-pqm-message='%s'" % (
4082- pickle.dumps(
4083- self.message).encode(
4084- 'base64').encode('string-escape'),))
4085-
4086- # Do we want to disconnect the terminal once the test run starts?
4087- if self.headless:
4088- cmd.append('--daemon')
4089-
4090- # Which branch do we want to test?
4091- if self._branch is not None:
4092- branch = self._branch
4093- remote_branch = Branch.open(branch)
4094- branch_revno = remote_branch.revno()
4095- else:
4096- branch = self._trunk_branch
4097- branch_revno = None
4098- cmd.append('--public-branch=%s' % branch)
4099- if branch_revno is not None:
4100- cmd.append('--public-branch-revno=%d' % branch_revno)
4101-
4102- # Add any additional options for ec2test-remote.py
4103- cmd.extend(['--', self.test_options])
4104- return ' '.join(cmd)
4105-
4106- def run_tests(self):
4107- self.configure_system()
4108- self.prepare_tests()
4109-
4110- self.log(
4111- 'Running tests... (output is available on '
4112- 'http://%s/)\n' % self._instance.hostname)
4113-
4114- # Try opening a browser pointed at the current test results.
4115- if self.open_browser:
4116- try:
4117- import webbrowser
4118- except ImportError:
4119- self.log("Could not open web browser due to ImportError.")
4120- else:
4121- status = webbrowser.open(self._instance.hostname)
4122- if not status:
4123- self.log("Could not open web browser.")
4124-
4125- # Run the remote script! Our execution will block here until the
4126- # remote side disconnects from the terminal.
4127- cmd = self._build_command()
4128- user_connection = self._instance.connect()
4129- user_connection.perform(cmd)
4130- self._running = True
4131-
4132- if not self.headless:
4133- # We ran to completion locally, so we'll be in charge of shutting
4134- # down the instance, in case the user has requested a postmortem.
4135- #
4136- # We only have 60 seconds to do this before the remote test
4137- # script shuts the server down automatically.
4138- user_connection.perform(
4139- 'kill `cat /var/launchpad/ec2test-remote.pid`')
4140-
4141- # deliver results as requested
4142- if self.file:
4143- self.log(
4144- 'Writing abridged test results to %s.\n' % self.file)
4145- user_connection.sftp.get('/var/www/summary.log', self.file)
4146- user_connection.close()
4147
4148=== removed directory 'lib/devscripts/ec2test/tests'
4149=== removed file 'lib/devscripts/ec2test/tests/__init__.py'
4150--- lib/devscripts/ec2test/tests/__init__.py 2009-10-05 19:20:37 +0000
4151+++ lib/devscripts/ec2test/tests/__init__.py 1970-01-01 00:00:00 +0000
4152@@ -1,2 +0,0 @@
4153-# Copyright 2009 Canonical Ltd. This software is licensed under the
4154-# GNU Affero General Public License version 3 (see the file LICENSE).
4155
4156=== removed file 'lib/devscripts/ec2test/tests/remote_daemonization_test.py'
4157--- lib/devscripts/ec2test/tests/remote_daemonization_test.py 2012-01-01 03:03:28 +0000
4158+++ lib/devscripts/ec2test/tests/remote_daemonization_test.py 1970-01-01 00:00:00 +0000
4159@@ -1,53 +0,0 @@
4160-# Copyright 2010 Canonical Ltd. This software is licensed under the
4161-# GNU Affero General Public License version 3 (see the file LICENSE).
4162-
4163-"""Script executed by test_remote.py to verify daemonization behaviour.
4164-
4165-See TestDaemonizationInteraction.
4166-"""
4167-
4168-import os
4169-import sys
4170-import traceback
4171-
4172-from devscripts.ec2test.remote import (
4173- EC2Runner,
4174- WebTestLogger,
4175- )
4176-from devscripts.ec2test.tests.test_remote import TestRequest
4177-
4178-
4179-PID_FILENAME = os.path.abspath(sys.argv[1])
4180-DIRECTORY = os.path.abspath(sys.argv[2])
4181-LOG_FILENAME = os.path.abspath(sys.argv[3])
4182-
4183-
4184-def make_request():
4185- """Just make a request."""
4186- test = TestRequest('test_wants_email')
4187- test.setUp()
4188- try:
4189- return test.make_request()
4190- finally:
4191- test.tearDown()
4192-
4193-
4194-def prepare_files(logger):
4195- try:
4196- logger.prepare()
4197- except:
4198- # If anything in the above fails, we want to be able to find out about
4199- # it. We can't use stdout or stderr because this is a daemon.
4200- error_log = open(LOG_FILENAME, 'w')
4201- traceback.print_exc(file=error_log)
4202- error_log.close()
4203-
4204-
4205-request = make_request()
4206-os.mkdir(DIRECTORY)
4207-logger = WebTestLogger.make_in_directory(DIRECTORY, request, True)
4208-runner = EC2Runner(
4209- daemonize=True,
4210- pid_filename=PID_FILENAME,
4211- shutdown_when_done=False)
4212-runner.run("test daemonization interaction", prepare_files, logger)
4213
4214=== removed file 'lib/devscripts/ec2test/tests/test_ec2instance.py'
4215--- lib/devscripts/ec2test/tests/test_ec2instance.py 2012-01-01 03:03:28 +0000
4216+++ lib/devscripts/ec2test/tests/test_ec2instance.py 1970-01-01 00:00:00 +0000
4217@@ -1,140 +0,0 @@
4218-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
4219-# GNU Affero General Public License version 3 (see the file LICENSE).
4220-# pylint: disable-msg=E0702
4221-
4222-"""Test handling of EC2 machine images."""
4223-
4224-__metaclass__ = type
4225-
4226-from unittest import TestCase
4227-
4228-from devscripts.ec2test.instance import EC2Instance
4229-from lp.testing.fakemethod import FakeMethod
4230-
4231-
4232-class FakeAccount:
4233- """Helper for setting up an `EC2Instance` without EC2."""
4234- acquire_private_key = FakeMethod()
4235- acquire_security_group = FakeMethod()
4236-
4237-
4238-class FakeOutput:
4239- """Pretend stdout/stderr output from EC2 instance."""
4240- output = "Fake output."
4241-
4242-
4243-class FakeBotoInstance:
4244- """Helper for setting up an `EC2Instance` without EC2."""
4245- id = 0
4246- state = 'running'
4247- public_dns_name = 'fake-instance'
4248-
4249- update = FakeMethod()
4250- stop = FakeMethod()
4251- get_console_output = FakeOutput
4252-
4253-
4254-class FakeReservation:
4255- """Helper for setting up an `EC2Instance` without EC2."""
4256- def __init__(self):
4257- self.instances = [FakeBotoInstance()]
4258-
4259-
4260-class FakeImage:
4261- """Helper for setting up an `EC2Instance` without EC2."""
4262- run = FakeMethod(result=FakeReservation())
4263-
4264-
4265-class FakeFailure(Exception):
4266- """A pretend failure from the test runner."""
4267-
4268-
4269-class TestEC2Instance(TestCase):
4270- """Test running of an `EC2Instance` without EC2."""
4271-
4272- def _makeInstance(self):
4273- """Set up an `EC2Instance`, with stubbing where needed.
4274-
4275- `EC2Instance.shutdown` is replaced with a `FakeMethod`, so check
4276- its call_count to see whether it's been invoked.
4277- """
4278- session_name = None
4279- image = FakeImage()
4280- instance_type = 'c1.xlarge'
4281- demo_networks = None
4282- account = FakeAccount()
4283- from_scratch = None
4284- user_key = None
4285- login = None
4286- region = None
4287-
4288- instance = EC2Instance(
4289- session_name, image, instance_type, demo_networks, account,
4290- from_scratch, user_key, login,
4291- region)
4292-
4293- instance.shutdown = FakeMethod()
4294- instance._report_traceback = FakeMethod()
4295- instance.log = FakeMethod()
4296-
4297- return instance
4298-
4299- def _runInstance(self, instance, runnee=None, headless=False):
4300- """Set up and run an `EC2Instance` (but without EC2)."""
4301- if runnee is None:
4302- runnee = FakeMethod()
4303-
4304- instance.set_up_and_run(False, not headless, runnee)
4305-
4306- def test_EC2Instance_test_baseline(self):
4307- # The EC2 instances we set up have neither started nor been shut
4308- # down. After running, they have started.
4309- # Not a very useful test, except it establishes the basic
4310- # assumptions for the other tests.
4311- instance = self._makeInstance()
4312- runnee = FakeMethod()
4313-
4314- self.assertEqual(0, runnee.call_count)
4315- self.assertEqual(0, instance.shutdown.call_count)
4316-
4317- self._runInstance(instance, runnee=runnee)
4318-
4319- self.assertEqual(1, runnee.call_count)
4320-
4321- def test_set_up_and_run_headful(self):
4322- # A non-headless run executes all tests in the instance, then
4323- # shuts down.
4324- instance = self._makeInstance()
4325-
4326- self._runInstance(instance, headless=False)
4327-
4328- self.assertEqual(1, instance.shutdown.call_count)
4329-
4330- def test_set_up_and_run_headless(self):
4331- # An asynchronous, headless run kicks off the tests on the
4332- # instance but does not shut it down.
4333- instance = self._makeInstance()
4334-
4335- self._runInstance(instance, headless=True)
4336-
4337- self.assertEqual(0, instance.shutdown.call_count)
4338-
4339- def test_set_up_and_run_headful_failure(self):
4340- # If the test runner barfs, the instance swallows the exception
4341- # and shuts down.
4342- instance = self._makeInstance()
4343- runnee = FakeMethod(failure=FakeFailure("Headful barfage."))
4344-
4345- self._runInstance(instance, runnee=runnee, headless=False)
4346-
4347- self.assertEqual(1, instance.shutdown.call_count)
4348-
4349- def test_set_up_and_run_headless_failure(self):
4350- # If the instance's test runner fails to set up for a headless
4351- # run, the instance swallows the exception and shuts down.
4352- instance = self._makeInstance()
4353- runnee = FakeMethod(failure=FakeFailure("Headless boom."))
4354-
4355- self._runInstance(instance, runnee=runnee, headless=True)
4356-
4357- self.assertEqual(1, instance.shutdown.call_count)
4358
4359=== removed file 'lib/devscripts/ec2test/tests/test_remote.py'
4360--- lib/devscripts/ec2test/tests/test_remote.py 2012-01-01 03:03:28 +0000
4361+++ lib/devscripts/ec2test/tests/test_remote.py 1970-01-01 00:00:00 +0000
4362@@ -1,1084 +0,0 @@
4363-# Copyright 2010 Canonical Ltd. This software is licensed under the
4364-# GNU Affero General Public License version 3 (see the file LICENSE).
4365-
4366-"""Tests for the script run on the remote server."""
4367-
4368-__metaclass__ = type
4369-
4370-from datetime import (
4371- datetime,
4372- timedelta,
4373- )
4374-import doctest
4375-from email.mime.application import MIMEApplication
4376-from email.mime.text import MIMEText
4377-import gzip
4378-from itertools import izip
4379-import os
4380-from StringIO import StringIO
4381-import subprocess
4382-import sys
4383-import tempfile
4384-import time
4385-import traceback
4386-import unittest
4387-
4388-from bzrlib.config import GlobalConfig
4389-from bzrlib.tests import TestCaseWithTransport
4390-import simplejson
4391-from testtools import (
4392- TestCase,
4393- TestResult,
4394- )
4395-from testtools.content import Content
4396-from testtools.content_type import ContentType
4397-from testtools.matchers import DocTestMatches
4398-
4399-from devscripts.ec2test.remote import (
4400- EC2Runner,
4401- FailureUpdateResult,
4402- gunzip_data,
4403- gzip_data,
4404- LaunchpadTester,
4405- remove_pidfile,
4406- Request,
4407- SummaryResult,
4408- WebTestLogger,
4409- write_pidfile,
4410- )
4411-
4412-
4413-class LoggingSMTPConnection(object):
4414- """An SMTPConnection double that logs sent email."""
4415-
4416- def __init__(self, log):
4417- self._log = log
4418-
4419- def send_email(self, message):
4420- self._log.append(message)
4421-
4422-
4423-class RequestHelpers:
4424-
4425- def patch(self, obj, name, value):
4426- orig = getattr(obj, name)
4427- setattr(obj, name, value)
4428- self.addCleanup(setattr, obj, name, orig)
4429- return orig
4430-
4431- def make_trunk(self, parent_url='http://example.com/bzr/trunk'):
4432- """Make a trunk branch suitable for use with `Request`.
4433-
4434- `Request` expects to be given a path to a working tree that has a
4435- branch with a configured parent URL, so this helper returns such a
4436- working tree.
4437- """
4438- nick = parent_url.strip('/').split('/')[-1]
4439- tree = self.make_branch_and_tree(nick)
4440- tree.branch.set_parent(parent_url)
4441- return tree
4442-
4443- def make_request(self, branch_url=None, revno=None,
4444- trunk=None, sourcecode_path=None,
4445- emails=None, pqm_message=None, emails_sent=None):
4446- """Make a request to test, specifying only things we care about.
4447-
4448- Note that the returned request object will not ever send email, but
4449- will instead append "sent" emails to the list provided here as
4450- 'emails_sent'.
4451- """
4452- if trunk is None:
4453- trunk = self.make_trunk()
4454- if sourcecode_path is None:
4455- sourcecode_path = self.make_sourcecode(
4456- [('a', 'http://example.com/bzr/a', 2),
4457- ('b', 'http://example.com/bzr/b', 3),
4458- ('c', 'http://example.com/bzr/c', 5)])
4459- if emails_sent is None:
4460- emails_sent = []
4461- smtp_connection = LoggingSMTPConnection(emails_sent)
4462- request = Request(
4463- branch_url, revno, trunk.basedir, sourcecode_path, emails,
4464- pqm_message, smtp_connection)
4465- return request
4466-
4467- def make_sourcecode(self, branches):
4468- """Make a sourcecode directory with sample branches.
4469-
4470- :param branches: A list of (name, parent_url, revno) tuples.
4471- :return: The path to the sourcecode directory.
4472- """
4473- self.build_tree(['sourcecode/'])
4474- for name, parent_url, revno in branches:
4475- tree = self.make_branch_and_tree('sourcecode/%s' % (name,))
4476- tree.branch.set_parent(parent_url)
4477- for i in range(revno):
4478- tree.commit(message=str(i))
4479- return 'sourcecode/'
4480-
4481- def make_tester(self, logger=None, test_directory=None, test_options=()):
4482- if not logger:
4483- logger = self.make_logger()
4484- return LaunchpadTester(logger, test_directory, test_options)
4485-
4486- def make_logger(self, request=None, echo_to_stdout=False):
4487- if request is None:
4488- request = self.make_request()
4489- return WebTestLogger(
4490- 'full.log', 'summary.log', 'index.html', request, echo_to_stdout)
4491-
4492-
4493-class TestSummaryResult(TestCase):
4494- """Tests for `SummaryResult`."""
4495-
4496- def makeException(self, factory=None, *args, **kwargs):
4497- if factory is None:
4498- factory = RuntimeError
4499- try:
4500- raise factory(*args, **kwargs)
4501- except:
4502- return sys.exc_info()
4503-
4504- def test_formatError(self):
4505- # SummaryResult._formatError() combines the name of the test, the kind
4506- # of error and the details of the error in a nicely-formatted way.
4507- result = SummaryResult(None)
4508- output = result._formatError('FOO', 'test', 'error')
4509- expected = '%s\nFOO: test\n%s\nerror\n' % (
4510- result.double_line, result.single_line)
4511- self.assertEqual(expected, output)
4512-
4513- def test_addError(self):
4514- # SummaryResult.addError doesn't write immediately.
4515- stream = StringIO()
4516- test = self
4517- error = self.makeException()
4518- result = SummaryResult(stream)
4519- expected = result._formatError(
4520- 'ERROR', test, result._exc_info_to_string(error, test))
4521- result.addError(test, error)
4522- self.assertEqual(expected, stream.getvalue())
4523-
4524- def test_addFailure_does_not_write_immediately(self):
4525- # SummaryResult.addFailure doesn't write immediately.
4526- stream = StringIO()
4527- test = self
4528- error = self.makeException()
4529- result = SummaryResult(stream)
4530- expected = result._formatError(
4531- 'FAILURE', test, result._exc_info_to_string(error, test))
4532- result.addFailure(test, error)
4533- self.assertEqual(expected, stream.getvalue())
4534-
4535- def test_stopTest_flushes_stream(self):
4536- # SummaryResult.stopTest() flushes the stream.
4537- stream = StringIO()
4538- flush_calls = []
4539- stream.flush = lambda: flush_calls.append(None)
4540- result = SummaryResult(stream)
4541- result.stopTest(self)
4542- self.assertEqual(1, len(flush_calls))
4543-
4544-
4545-class TestFailureUpdateResult(TestCaseWithTransport, RequestHelpers):
4546-
4547- def makeException(self, factory=None, *args, **kwargs):
4548- if factory is None:
4549- factory = RuntimeError
4550- try:
4551- raise factory(*args, **kwargs)
4552- except:
4553- return sys.exc_info()
4554-
4555- def test_addError_is_unsuccessful(self):
4556- logger = self.make_logger()
4557- result = FailureUpdateResult(logger)
4558- result.addError(self, self.makeException())
4559- self.assertEqual(False, logger.successful)
4560-
4561- def test_addFailure_is_unsuccessful(self):
4562- logger = self.make_logger()
4563- result = FailureUpdateResult(logger)
4564- result.addFailure(self, self.makeException(AssertionError))
4565- self.assertEqual(False, logger.successful)
4566-
4567-
4568-class FakePopen:
4569- """Fake Popen object so we don't have to spawn processes in tests."""
4570-
4571- def __init__(self, output, exit_status):
4572- self.stdout = StringIO(output)
4573- self._exit_status = exit_status
4574-
4575- def wait(self):
4576- return self._exit_status
4577-
4578-
4579-class TestLaunchpadTester(TestCaseWithTransport, RequestHelpers):
4580-
4581- def test_build_test_command_no_options(self):
4582- # The LaunchpadTester runs "make check" if given no options.
4583- tester = self.make_tester()
4584- command = tester.build_test_command()
4585- self.assertEqual(['make', 'check'], command)
4586-
4587- def test_build_test_command_options(self):
4588- # The LaunchpadTester runs 'make check TESTOPTIONS="<options>"' if
4589- # given options.
4590- tester = self.make_tester(test_options=('-vvv', '--subunit'))
4591- command = tester.build_test_command()
4592- self.assertEqual(
4593- ['make', 'check', 'TESTOPTS="-vvv --subunit"'], command)
4594-
4595- def test_spawn_test_process(self):
4596- # _spawn_test_process uses subprocess.Popen to run the command
4597- # returned by build_test_command. stdout & stderr are piped together,
4598- # the cwd is the test directory specified in the constructor, and the
4599- # bufsize is zore, meaning "don't buffer".
4600- popen_calls = []
4601- self.patch(
4602- subprocess, 'Popen',
4603- lambda *args, **kwargs: popen_calls.append((args, kwargs)))
4604- tester = self.make_tester(test_directory='test-directory')
4605- tester._spawn_test_process()
4606- self.assertEqual(
4607- [((tester.build_test_command(),),
4608- {'bufsize': 0,
4609- 'stdout': subprocess.PIPE,
4610- 'stderr': subprocess.STDOUT,
4611- 'cwd': 'test-directory'})], popen_calls)
4612-
4613- def test_running_test(self):
4614- # LaunchpadTester.test() runs the test command, and then calls
4615- # got_result with the result. This test is more of a smoke test to
4616- # make sure that everything integrates well.
4617- message = {'Subject': "One Crowded Hour"}
4618- log = []
4619- request = self.make_request(pqm_message=message, emails_sent=log)
4620- logger = self.make_logger(request=request)
4621- tester = self.make_tester(logger=logger)
4622- output = "test output\n"
4623- tester._spawn_test_process = lambda: FakePopen(output, 0)
4624- tester.test()
4625- # Message being sent implies got_result thought it got a success.
4626- self.assertEqual([message], log)
4627-
4628- def test_failing_test(self):
4629- # If LaunchpadTester gets a failing test, then it records that on the
4630- # logger.
4631- logger = self.make_logger()
4632- tester = self.make_tester(logger=logger)
4633- output = "test: foo\nerror: foo\n"
4634- tester._spawn_test_process = lambda: FakePopen(output, 0)
4635- tester.test()
4636- self.assertEqual(False, logger.successful)
4637-
4638- def test_error_in_testrunner(self):
4639- # Any exception is raised within LaunchpadTester.test() is an error in
4640- # the testrunner. When we detect these, we do three things:
4641- # 1. Log the error to the logger using error_in_testrunner
4642- # 2. Call got_result with a False value, indicating test suite
4643- # failure.
4644- # 3. Re-raise the error. In the script, this triggers an email.
4645- message = {'Subject': "One Crowded Hour"}
4646- log = []
4647- request = self.make_request(pqm_message=message, emails_sent=log)
4648- logger = self.make_logger(request=request)
4649- tester = self.make_tester(logger=logger)
4650- # Break the test runner deliberately. In production, this is more
4651- # likely to be a system error than a programming error.
4652- tester._spawn_test_process = lambda: 1/0
4653- tester.test()
4654- # Message not being sent implies got_result thought it got a failure.
4655- self.assertEqual([], log)
4656- self.assertIn("ERROR IN TESTRUNNER", logger.get_summary_contents())
4657- self.assertIn("ZeroDivisionError", logger.get_summary_contents())
4658-
4659- def test_nonzero_exit_code(self):
4660- message = {'Subject': "One Crowded Hour"}
4661- log = []
4662- request = self.make_request(pqm_message=message, emails_sent=log)
4663- logger = self.make_logger(request=request)
4664- tester = self.make_tester(logger=logger)
4665- output = "test output\n"
4666- tester._spawn_test_process = lambda: FakePopen(output, 10)
4667- tester.test()
4668- # Message not being sent implies got_result thought it got a failure.
4669- self.assertEqual([], log)
4670-
4671- def test_gather_test_output(self):
4672- # LaunchpadTester._gather_test_output() summarises the output
4673- # stream as a TestResult.
4674- logger = self.make_logger()
4675- tester = self.make_tester(logger=logger)
4676- result = tester._gather_test_output(
4677- ['test: test_failure', 'failure: test_failure',
4678- 'test: test_success', 'successful: test_success'],
4679- logger)
4680- self.assertEquals(2, result.testsRun)
4681- self.assertEquals(1, len(result.failures))
4682-
4683-
4684-class TestPidfileHelpers(TestCase):
4685- """Tests for `write_pidfile` and `remove_pidfile`."""
4686-
4687- def test_write_pidfile(self):
4688- fd, path = tempfile.mkstemp()
4689- self.addCleanup(os.unlink, path)
4690- os.close(fd)
4691- write_pidfile(path)
4692- self.assertEqual(os.getpid(), int(open(path, 'r').read()))
4693-
4694- def test_remove_pidfile(self):
4695- fd, path = tempfile.mkstemp()
4696- os.close(fd)
4697- write_pidfile(path)
4698- remove_pidfile(path)
4699- self.assertEqual(False, os.path.exists(path))
4700-
4701- def test_remove_nonexistent_pidfile(self):
4702- directory = tempfile.mkdtemp()
4703- path = os.path.join(directory, 'doesntexist')
4704- remove_pidfile(path)
4705- self.assertEqual(False, os.path.exists(path))
4706-
4707-
4708-class TestGzip(TestCase):
4709- """Tests for gzip helpers."""
4710-
4711- def test_gzip_data(self):
4712- data = 'foobarbaz\n'
4713- compressed = gzip_data(data)
4714- fd, path = tempfile.mkstemp()
4715- os.write(fd, compressed)
4716- os.close(fd)
4717- self.assertEqual(data, gzip.open(path, 'r').read())
4718-
4719- def test_gunzip_data(self):
4720- data = 'foobarbaz\n'
4721- compressed = gzip_data(data)
4722- self.assertEqual(data, gunzip_data(compressed))
4723-
4724-
4725-class TestRequest(TestCaseWithTransport, RequestHelpers):
4726- """Tests for `Request`."""
4727-
4728- def test_doesnt_want_email(self):
4729- # If no email addresses were provided, then the user does not want to
4730- # receive email.
4731- req = self.make_request()
4732- self.assertEqual(False, req.wants_email)
4733-
4734- def test_wants_email(self):
4735- # If some email addresses were provided, then the user wants to
4736- # receive email.
4737- req = self.make_request(emails=['foo@example.com'])
4738- self.assertEqual(True, req.wants_email)
4739-
4740- def test_get_target_details(self):
4741- parent = 'http://example.com/bzr/branch'
4742- tree = self.make_trunk(parent)
4743- req = self.make_request(trunk=tree)
4744- self.assertEqual(
4745- (parent, tree.branch.revno()), req.get_target_details())
4746-
4747- def test_get_revno_target_only(self):
4748- # If there's only a target branch, then the revno is the revno of that
4749- # branch.
4750- parent = 'http://example.com/bzr/branch'
4751- tree = self.make_trunk(parent)
4752- req = self.make_request(trunk=tree)
4753- self.assertEqual(tree.branch.revno(), req.get_revno())
4754-
4755- def test_get_revno_source_and_target(self):
4756- # If we're merging in a branch, then the revno is the revno of the
4757- # branch we're merging in.
4758- tree = self.make_trunk()
4759- # Fake a merge, giving silly revision ids.
4760- tree.add_pending_merge('foo', 'bar')
4761- req = self.make_request(
4762- branch_url='https://example.com/bzr/thing', revno=42, trunk=tree)
4763- self.assertEqual(42, req.get_revno())
4764-
4765- def test_get_source_details_no_commits(self):
4766- req = self.make_request(trunk=self.make_trunk())
4767- self.assertEqual(None, req.get_source_details())
4768-
4769- def test_get_source_details_no_merge(self):
4770- tree = self.make_trunk()
4771- tree.commit(message='foo')
4772- req = self.make_request(trunk=tree)
4773- self.assertEqual(None, req.get_source_details())
4774-
4775- def test_get_source_details_merge(self):
4776- tree = self.make_trunk()
4777- # Fake a merge, giving silly revision ids.
4778- tree.add_pending_merge('foo', 'bar')
4779- req = self.make_request(
4780- branch_url='https://example.com/bzr/thing', revno=42, trunk=tree)
4781- self.assertEqual(
4782- ('https://example.com/bzr/thing', 42), req.get_source_details())
4783-
4784- def test_get_nick_trunk_only(self):
4785- tree = self.make_trunk(parent_url='http://example.com/bzr/db-devel')
4786- req = self.make_request(trunk=tree)
4787- self.assertEqual('db-devel', req.get_nick())
4788-
4789- def test_get_nick_merge(self):
4790- tree = self.make_trunk()
4791- # Fake a merge, giving silly revision ids.
4792- tree.add_pending_merge('foo', 'bar')
4793- req = self.make_request(
4794- branch_url='https://example.com/bzr/thing', revno=42, trunk=tree)
4795- self.assertEqual('thing', req.get_nick())
4796-
4797- def test_get_merge_description_trunk_only(self):
4798- tree = self.make_trunk(parent_url='http://example.com/bzr/db-devel')
4799- req = self.make_request(trunk=tree)
4800- self.assertEqual(
4801- 'db-devel r%s' % req.get_revno(), req.get_merge_description())
4802-
4803- def test_get_merge_description_merge(self):
4804- tree = self.make_trunk(parent_url='http://example.com/bzr/db-devel/')
4805- tree.add_pending_merge('foo', 'bar')
4806- req = self.make_request(
4807- branch_url='https://example.com/bzr/thing', revno=42, trunk=tree)
4808- self.assertEqual('thing => db-devel', req.get_merge_description())
4809-
4810- def test_get_summary_commit(self):
4811- # The summary commit message is the last commit message of the branch
4812- # we're merging in.
4813- trunk = self.make_trunk()
4814- trunk.commit(message="a starting point")
4815- thing_bzrdir = trunk.branch.bzrdir.sprout('thing')
4816- thing = thing_bzrdir.open_workingtree()
4817- thing.commit(message="a new thing")
4818- trunk.merge_from_branch(thing.branch)
4819- req = self.make_request(
4820- branch_url='https://example.com/bzr/thing',
4821- revno=thing.branch.revno(),
4822- trunk=trunk)
4823- self.assertEqual("a new thing", req.get_summary_commit())
4824-
4825- def test_iter_dependency_branches(self):
4826- # iter_dependency_branches yields a list of branches in the sourcecode
4827- # directory, along with their parent URLs and their revnos.
4828- sourcecode_branches = [
4829- ('b', 'http://example.com/parent-b', 3),
4830- ('a', 'http://example.com/parent-a', 2),
4831- ('c', 'http://example.com/parent-c', 5),
4832- ]
4833- sourcecode_path = self.make_sourcecode(sourcecode_branches)
4834- self.build_tree(
4835- ['%s/not-a-branch/' % sourcecode_path,
4836- '%s/just-a-file' % sourcecode_path])
4837- req = self.make_request(sourcecode_path=sourcecode_path)
4838- branches = list(req.iter_dependency_branches())
4839- self.assertEqual(sorted(sourcecode_branches), branches)
4840-
4841- def test_submit_to_pqm_no_message(self):
4842- # If there's no PQM message, then 'submit_to_pqm' returns None.
4843- req = self.make_request(pqm_message=None)
4844- subject = req.submit_to_pqm(successful=True)
4845- self.assertIs(None, subject)
4846-
4847- def test_submit_to_pqm_no_message_doesnt_send(self):
4848- # If there's no PQM message, then 'submit_to_pqm' returns None.
4849- log = []
4850- req = self.make_request(pqm_message=None, emails_sent=log)
4851- req.submit_to_pqm(successful=True)
4852- self.assertEqual([], log)
4853-
4854- def test_submit_to_pqm_unsuccessful(self):
4855- # submit_to_pqm returns the subject of the PQM mail even if it's
4856- # handling a failed test run.
4857- message = {'Subject': 'My PQM message'}
4858- req = self.make_request(pqm_message=message)
4859- subject = req.submit_to_pqm(successful=False)
4860- self.assertIs(message.get('Subject'), subject)
4861-
4862- def test_submit_to_pqm_unsuccessful_no_email(self):
4863- # submit_to_pqm doesn't send any email if the run was unsuccessful.
4864- message = {'Subject': 'My PQM message'}
4865- log = []
4866- req = self.make_request(pqm_message=message, emails_sent=log)
4867- req.submit_to_pqm(successful=False)
4868- self.assertEqual([], log)
4869-
4870- def test_submit_to_pqm_successful(self):
4871- # submit_to_pqm returns the subject of the PQM mail.
4872- message = {'Subject': 'My PQM message'}
4873- log = []
4874- req = self.make_request(pqm_message=message, emails_sent=log)
4875- subject = req.submit_to_pqm(successful=True)
4876- self.assertIs(message.get('Subject'), subject)
4877- self.assertEqual([message], log)
4878-
4879- def test_report_email_subject_success(self):
4880- req = self.make_request(emails=['foo@example.com'])
4881- email = req._build_report_email(True, 'foo', 'gobbledygook')
4882- self.assertEqual(
4883- 'Test results: %s: SUCCESS' % req.get_merge_description(),
4884- email['Subject'])
4885-
4886- def test_report_email_subject_failure(self):
4887- req = self.make_request(emails=['foo@example.com'])
4888- email = req._build_report_email(False, 'foo', 'gobbledygook')
4889- self.assertEqual(
4890- 'Test results: %s: FAILURE' % req.get_merge_description(),
4891- email['Subject'])
4892-
4893- def test_report_email_recipients(self):
4894- req = self.make_request(emails=['foo@example.com', 'bar@example.com'])
4895- email = req._build_report_email(False, 'foo', 'gobbledygook')
4896- self.assertEqual('foo@example.com, bar@example.com', email['To'])
4897-
4898- def test_report_email_sender(self):
4899- req = self.make_request(emails=['foo@example.com'])
4900- email = req._build_report_email(False, 'foo', 'gobbledygook')
4901- self.assertEqual(GlobalConfig().username(), email['From'])
4902-
4903- def test_report_email_body(self):
4904- req = self.make_request(emails=['foo@example.com'])
4905- email = req._build_report_email(False, 'foo', 'gobbledygook')
4906- [body, attachment] = email.get_payload()
4907- self.assertIsInstance(body, MIMEText)
4908- self.assertEqual('inline', body['Content-Disposition'])
4909- self.assertIn(
4910- body['Content-Type'],
4911- ['text/plain; charset="utf-8"', 'text/plain; charset="utf8"'])
4912- self.assertEqual("foo", body.get_payload(decode=True))
4913-
4914- def test_report_email_attachment(self):
4915- req = self.make_request(emails=['foo@example.com'])
4916- email = req._build_report_email(False, "foo", "gobbledygook")
4917- [body, attachment] = email.get_payload()
4918- self.assertIsInstance(attachment, MIMEApplication)
4919- self.assertEqual('application/x-gzip', attachment['Content-Type'])
4920- self.assertEqual(
4921- 'attachment; filename="%s-r%s.subunit.gz"' % (
4922- req.get_nick(), req.get_revno()),
4923- attachment['Content-Disposition'])
4924- self.assertEqual(
4925- "gobbledygook", attachment.get_payload(decode=True))
4926-
4927- def test_send_report_email_sends_email(self):
4928- log = []
4929- req = self.make_request(emails=['foo@example.com'], emails_sent=log)
4930- expected = req._build_report_email(False, "foo", "gobbledygook")
4931- req.send_report_email(False, "foo", "gobbledygook")
4932- [observed] = log
4933- # The standard library sucks. None of the MIME objects have __eq__
4934- # implementations.
4935- for expected_part, observed_part in izip(
4936- expected.walk(), observed.walk()):
4937- self.assertEqual(type(expected_part), type(observed_part))
4938- self.assertEqual(expected_part.items(), observed_part.items())
4939- self.assertEqual(
4940- expected_part.is_multipart(), observed_part.is_multipart())
4941- if not expected_part.is_multipart():
4942- self.assertEqual(
4943- expected_part.get_payload(), observed_part.get_payload())
4944-
4945- def test_format_result_success(self):
4946-
4947- class SomeTest(TestCase):
4948-
4949- def test_a(self):
4950- pass
4951-
4952- def test_b(self):
4953- pass
4954-
4955- def test_c(self):
4956- pass
4957-
4958- test = unittest.TestSuite(map(SomeTest, ['test_' + x for x in 'abc']))
4959- result = TestResult()
4960- test.run(result)
4961- tree = self.make_trunk()
4962- # Fake a merge, giving silly revision ids.
4963- tree.add_pending_merge('foo', 'bar')
4964- req = self.make_request(
4965- branch_url='https://example.com/bzr/thing', revno=42, trunk=tree)
4966- source_branch, source_revno = req.get_source_details()
4967- target_branch, target_revno = req.get_target_details()
4968- start_time = datetime.utcnow()
4969- end_time = start_time + timedelta(hours=1)
4970- data = {
4971- 'source_branch': source_branch,
4972- 'source_revno': source_revno,
4973- 'target_branch': target_branch,
4974- 'target_revno': target_revno,
4975- 'start_time': str(start_time),
4976- 'duration': str(end_time - start_time),
4977- 'num_tests': result.testsRun,
4978- 'num_failures': len(result.failures),
4979- 'num_errors': len(result.errors),
4980- }
4981- result_text = req.format_result(result, start_time, end_time)
4982- self.assertThat(
4983- result_text, DocTestMatches("""\
4984-Tests started at approximately %(start_time)s
4985-Source: %(source_branch)s r%(source_revno)s
4986-Target: %(target_branch)s r%(target_revno)s
4987-<BLANKLINE>
4988-%(num_tests)s tests run in %(duration)s, %(num_failures)s failures, \
4989-%(num_errors)s errors
4990-<BLANKLINE>
4991-(See the attached file for the complete log)
4992-""" % data, doctest.REPORT_NDIFF | doctest.ELLIPSIS))
4993-
4994- def test_format_result_with_errors(self):
4995-
4996- class SomeTest(TestCase):
4997-
4998- def test_ok(self):
4999- pass
5000-
The diff has been truncated for viewing.