Merge lp:~abentley/launchpad/upgrade-all-2 into lp:launchpad
- upgrade-all-2
- Merge into devel
| 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 |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Deryck Hodge (community) | 2012-03-06 | Approve on 2012-03-06 | |
|
Review via email:
|
|||
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/
lib/lp/
lib/lp/
scripts/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp_
lib/lp/
Preview Diff
| 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 | - |
