Merge lp:~allenap/launchpad/ec2-test-race-bug-422433-the-revenge into lp:launchpad
- ec2-test-race-bug-422433-the-revenge
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~allenap/launchpad/ec2-test-race-bug-422433-the-revenge |
Merge into: | lp:launchpad |
Diff against target: |
642 lines 10 files modified
lib/devscripts/ec2test/account.py (+58/-48) lib/devscripts/ec2test/builtins.py (+12/-7) lib/devscripts/ec2test/instance.py (+20/-15) lib/devscripts/ec2test/session.py (+90/-0) lib/devscripts/ec2test/tests/__init__.py (+2/-0) lib/devscripts/ec2test/tests/test_session.py (+69/-0) lib/devscripts/ec2test/tests/test_utils.py (+61/-0) lib/devscripts/ec2test/utils.py (+55/-0) setup.py (+1/-0) versions.cfg (+2/-0) |
To merge this branch: | bzr merge lp:~allenap/launchpad/ec2-test-race-bug-422433-the-revenge |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | code | Approve | |
Review via email: mp+13064@code.launchpad.net |
Commit message
Description of the change
Gavin Panella (allenap) wrote : | # |
Michael Nelson (michael.nelson) wrote : | # |
> This is a resubmission of an earlier branch:
>
> https:/
> bug-422433/+merge/12778
>
> It got merged, but had to be reverted because of failures in buildbot.
<noodles775> allenap: so the previous mp failed on buildbot because it didn't have paramiko?
* andrea-bs (n=andrea@
* henninge has quit (Read error: 113 (No route to host))
<allenap> noodles775: Yes, the buildbot AMI seems to be out of date, which is another bug. However, I thought it was a good idea to put the dep into the tree anyway.
<noodles775> allenap: ok, and where is pycrypto used?
<allenap> noodles775: It's required by paramiko :)
<noodles775> allenap: r=me
<allenap> noodles775: Thanks :)
Preview Diff
1 | === modified file 'lib/devscripts/ec2test/account.py' |
2 | --- lib/devscripts/ec2test/account.py 2009-10-03 08:19:54 +0000 |
3 | +++ lib/devscripts/ec2test/account.py 2009-10-08 14:53:12 +0000 |
4 | @@ -13,7 +13,10 @@ |
5 | import sys |
6 | import urllib |
7 | |
8 | +from datetime import datetime |
9 | + |
10 | from boto.exception import EC2ResponseError |
11 | +from devscripts.ec2test.session import EC2SessionName |
12 | |
13 | import paramiko |
14 | |
15 | @@ -24,6 +27,13 @@ |
16 | # ...anyone else want in on the fun? |
17 | ) |
18 | |
19 | +AUTH_FAILURE_MESSAGE = """\ |
20 | +POSSIBLE CAUSES OF ERROR: |
21 | +- Did you sign up for EC2? |
22 | +- Did you put a credit card number in your AWS account? |
23 | +Please double-check before reporting a problem. |
24 | +""" |
25 | + |
26 | |
27 | def get_ip(): |
28 | """Uses AWS checkip to obtain this machine's IP address. |
29 | @@ -64,44 +74,29 @@ |
30 | sys.stdout.write(msg) |
31 | sys.stdout.flush() |
32 | |
33 | + def _find_expired_artifacts(self, artifacts): |
34 | + now = datetime.utcnow() |
35 | + for artifact in artifacts: |
36 | + session_name = EC2SessionName(artifact.name) |
37 | + if (session_name in (self.name, self.name.base) or ( |
38 | + session_name.base == self.name.base and |
39 | + session_name.expires is not None and |
40 | + session_name.expires < now)): |
41 | + yield artifact |
42 | + |
43 | def acquire_security_group(self, demo_networks=None): |
44 | """Get a security group with the appropriate configuration. |
45 | |
46 | "Appropriate" means configured to allow this machine to connect via |
47 | SSH, HTTP and HTTPS. |
48 | |
49 | - If a group is already configured with this name for this connection, |
50 | - then re-use that. Otherwise, create a new security group and configure |
51 | - it appropriately. |
52 | - |
53 | The name of the security group is the `EC2Account.name` attribute. |
54 | |
55 | :return: A boto security group. |
56 | """ |
57 | if demo_networks is None: |
58 | demo_networks = [] |
59 | - try: |
60 | - group = self.conn.get_all_security_groups(self.name)[0] |
61 | - except EC2ResponseError, e: |
62 | - if e.code != 'InvalidGroup.NotFound': |
63 | - raise |
64 | - else: |
65 | - # If an existing security group was configured, try deleting it |
66 | - # since our external IP might have changed. |
67 | - try: |
68 | - group.delete() |
69 | - except EC2ResponseError, e: |
70 | - if e.code != 'InvalidGroup.InUse': |
71 | - raise |
72 | - # Otherwise, it means that an instance is already using |
73 | - # it, so simply re-use it. It's unlikely that our IP changed! |
74 | - # |
75 | - # XXX: JonathanLange 2009-06-05: If the security group exists |
76 | - # already, verify that the current IP is permitted; if it is |
77 | - # not, make an INFO log and add the current IP. |
78 | - self.log("Security group already in use, so reusing.\n") |
79 | - return group |
80 | - |
81 | + # Create the security group. |
82 | security_group = self.conn.create_security_group( |
83 | self.name, 'Authorization to access the test runner instance.') |
84 | # Authorize SSH and HTTP. |
85 | @@ -117,34 +112,49 @@ |
86 | security_group.authorize('tcp', 443, 443, network) |
87 | return security_group |
88 | |
89 | + def delete_previous_security_groups(self): |
90 | + """Delete previously used security groups, if found.""" |
91 | + expired_groups = self._find_expired_artifacts( |
92 | + self.conn.get_all_security_groups()) |
93 | + for group in expired_groups: |
94 | + try: |
95 | + group.delete() |
96 | + except EC2ResponseError, e: |
97 | + if e.code != 'InvalidGroup.InUse': |
98 | + raise |
99 | + self.log('Cannot delete; security group ' |
100 | + '%r in use.\n' % group.name) |
101 | + else: |
102 | + self.log('Deleted security group %r.\n' % group.name) |
103 | + |
104 | def acquire_private_key(self): |
105 | """Create & return a new key pair for the test runner.""" |
106 | key_pair = self.conn.create_key_pair(self.name) |
107 | return paramiko.RSAKey.from_private_key( |
108 | cStringIO.StringIO(key_pair.material.encode('ascii'))) |
109 | |
110 | - def delete_previous_key_pair(self): |
111 | - """Delete previously used keypair, if it exists.""" |
112 | - try: |
113 | - # Only one keypair will match 'self.name' since it's a unique |
114 | - # identifier. |
115 | - key_pairs = self.conn.get_all_key_pairs(self.name) |
116 | - assert len(key_pairs) == 1, ( |
117 | - "Should be only one keypair, found %d (%s)" |
118 | - % (len(key_pairs), key_pairs)) |
119 | - key_pair = key_pairs[0] |
120 | - key_pair.delete() |
121 | - except EC2ResponseError, e: |
122 | - if e.code != 'InvalidKeyPair.NotFound': |
123 | - if e.code == 'AuthFailure': |
124 | - # Inserted because of previous support issue. |
125 | - self.log( |
126 | - 'POSSIBLE CAUSES OF ERROR:\n' |
127 | - ' Did you sign up for EC2?\n' |
128 | - ' Did you put a credit card number in your AWS ' |
129 | - 'account?\n' |
130 | - 'Please doublecheck before reporting a problem.\n') |
131 | - raise |
132 | + def delete_previous_key_pairs(self): |
133 | + """Delete previously used keypairs, if found.""" |
134 | + expired_key_pairs = self._find_expired_artifacts( |
135 | + self.conn.get_all_key_pairs()) |
136 | + for key_pair in expired_key_pairs: |
137 | + try: |
138 | + key_pair.delete() |
139 | + except EC2ResponseError, e: |
140 | + if e.code != 'InvalidKeyPair.NotFound': |
141 | + if e.code == 'AuthFailure': |
142 | + # Inserted because of previous support issue. |
143 | + self.log(AUTH_FAILURE_MESSAGE) |
144 | + raise |
145 | + self.log('Cannot delete; key pair not ' |
146 | + 'found %r\n' % key_pair.name) |
147 | + else: |
148 | + self.log('Deleted key pair %r.\n' % key_pair.name) |
149 | + |
150 | + def collect_garbage(self): |
151 | + """Remove any old keys and security groups.""" |
152 | + self.delete_previous_security_groups() |
153 | + self.delete_previous_key_pairs() |
154 | |
155 | def acquire_image(self, machine_id): |
156 | """Get the image. |
157 | |
158 | === modified file 'lib/devscripts/ec2test/builtins.py' |
159 | --- lib/devscripts/ec2test/builtins.py 2009-10-05 13:31:40 +0000 |
160 | +++ lib/devscripts/ec2test/builtins.py 2009-10-08 14:53:12 +0000 |
161 | @@ -22,6 +22,7 @@ |
162 | from devscripts.ec2test.credentials import EC2Credentials |
163 | from devscripts.ec2test.instance import ( |
164 | AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE, EC2Instance) |
165 | +from devscripts.ec2test.session import EC2SessionName |
166 | from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH |
167 | |
168 | |
169 | @@ -240,8 +241,8 @@ |
170 | postmortem_option, |
171 | Option( |
172 | 'headless', |
173 | - help=('After building the instance and test, run the remote tests ' |
174 | - 'headless. Cannot be used with postmortem ' |
175 | + help=('After building the instance and test, run the remote ' |
176 | + 'tests headless. Cannot be used with postmortem ' |
177 | 'or file.')), |
178 | debug_option, |
179 | Option( |
180 | @@ -285,8 +286,9 @@ |
181 | "Submitting to PQM with non-default test options isn't " |
182 | "supported") |
183 | |
184 | + session_name = EC2SessionName.make(EC2TestRunner.name) |
185 | instance = EC2Instance.make( |
186 | - EC2TestRunner.name, instance_type, machine) |
187 | + session_name, instance_type, machine) |
188 | |
189 | runner = EC2TestRunner( |
190 | test_branch, email=email, file=file, |
191 | @@ -426,8 +428,9 @@ |
192 | branches, test_branch = _get_branches_and_test_branch( |
193 | trunk, branch, test_branch) |
194 | |
195 | + session_name = EC2SessionName.make(EC2TestRunner.name) |
196 | instance = EC2Instance.make( |
197 | - EC2TestRunner.name, instance_type, machine, demo) |
198 | + session_name, instance_type, machine, demo) |
199 | |
200 | runner = EC2TestRunner( |
201 | test_branch, branches=branches, |
202 | @@ -480,8 +483,9 @@ |
203 | ListOption( |
204 | 'extra-update-image-command', type=str, |
205 | help=('Run this command (with an ssh agent) on the image before ' |
206 | - 'running the default update steps. Can be passed more than ' |
207 | - 'once, the commands will be run in the order specified.')), |
208 | + 'running the default update steps. Can be passed more ' |
209 | + 'than once, the commands will be run in the order ' |
210 | + 'specified.')), |
211 | Option( |
212 | 'public', |
213 | help=('Remove proprietary code from the sourcecode directory ' |
214 | @@ -498,8 +502,9 @@ |
215 | |
216 | credentials = EC2Credentials.load_from_file() |
217 | |
218 | + session_name = EC2SessionName.make(EC2TestRunner.name) |
219 | instance = EC2Instance.make( |
220 | - EC2TestRunner.name, instance_type, machine, |
221 | + session_name, instance_type, machine, |
222 | credentials=credentials) |
223 | instance.check_bundling_prerequisites() |
224 | |
225 | |
226 | === modified file 'lib/devscripts/ec2test/instance.py' |
227 | --- lib/devscripts/ec2test/instance.py 2009-10-04 15:40:46 +0000 |
228 | +++ lib/devscripts/ec2test/instance.py 2009-10-08 14:53:12 +0000 |
229 | @@ -24,6 +24,7 @@ |
230 | import paramiko |
231 | |
232 | from devscripts.ec2test.credentials import EC2Credentials |
233 | +from devscripts.ec2test.session import EC2SessionName |
234 | |
235 | |
236 | DEFAULT_INSTANCE_TYPE = 'c1.xlarge' |
237 | @@ -160,6 +161,18 @@ |
238 | """ |
239 | |
240 | |
241 | +postmortem_banner = """\ |
242 | +Postmortem Console. EC2 instance is not yet dead. |
243 | +It will shut down when you exit this prompt (CTRL-D) |
244 | + |
245 | +Tab-completion is enabled. |
246 | +EC2Instance is available as `instance`. |
247 | +Also try these: |
248 | + http://%(dns)s/current_test.log |
249 | + ssh -A %(dns)s |
250 | +""" |
251 | + |
252 | + |
253 | class EC2Instance: |
254 | """A single EC2 instance.""" |
255 | |
256 | @@ -170,6 +183,7 @@ |
257 | |
258 | :param name: The name to use for the key pair and security group for |
259 | the instance. |
260 | + :type name: `EC2SessionName` |
261 | :param instance_type: One of the AVAILABLE_INSTANCE_TYPES. |
262 | :param machine_id: The AMI to use, or None to do the usual regexp |
263 | matching. If you put 'based-on:' before the AMI id, it is assumed |
264 | @@ -180,6 +194,7 @@ |
265 | to allow access to the instance. |
266 | :param credentials: An `EC2Credentials` object. |
267 | """ |
268 | + assert isinstance(name, EC2SessionName) |
269 | if instance_type not in AVAILABLE_INSTANCE_TYPES: |
270 | raise ValueError('unknown instance_type %s' % (instance_type,)) |
271 | |
272 | @@ -200,7 +215,7 @@ |
273 | # We always recreate the keypairs because there is no way to |
274 | # programmatically retrieve the private key component, unless we |
275 | # generate it. |
276 | - account.delete_previous_key_pair() |
277 | + account.collect_garbage() |
278 | |
279 | if machine_id and machine_id.startswith('based-on:'): |
280 | from_scratch = True |
281 | @@ -345,8 +360,8 @@ |
282 | if our_connection: |
283 | connection.close() |
284 | self.log( |
285 | - 'You can now use ssh -A ec2test@%s to log in the instance.\n' % |
286 | - self.hostname) |
287 | + 'You can now use ssh -A ec2test@%s to ' |
288 | + 'log in the instance.\n' % self.hostname) |
289 | self._ec2test_user_has_keys = True |
290 | |
291 | def connect(self): |
292 | @@ -401,18 +416,8 @@ |
293 | try: |
294 | if postmortem: |
295 | console = code.InteractiveConsole(locals()) |
296 | - console.interact(( |
297 | - 'Postmortem Console. EC2 instance is not yet dead.\n' |
298 | - 'It will shut down when you exit this prompt ' |
299 | - '(CTRL-D).\n' |
300 | - '\n' |
301 | - 'Tab-completion is enabled.' |
302 | - '\n' |
303 | - 'EC2Instance is available as `instance`.\n' |
304 | - 'Also try these:\n' |
305 | - ' http://%(dns)s/current_test.log\n' |
306 | - ' ssh -A %(dns)s') % |
307 | - {'dns': self.hostname}) |
308 | + console.interact( |
309 | + postmortem_banner % {'dns': self.hostname}) |
310 | print 'Postmortem console closed.' |
311 | finally: |
312 | if shutdown: |
313 | |
314 | === added file 'lib/devscripts/ec2test/session.py' |
315 | --- lib/devscripts/ec2test/session.py 1970-01-01 00:00:00 +0000 |
316 | +++ lib/devscripts/ec2test/session.py 2009-10-08 14:53:12 +0000 |
317 | @@ -0,0 +1,90 @@ |
318 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
319 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
320 | + |
321 | +"""Code to represent a single session of EC2 use.""" |
322 | + |
323 | +__metaclass__ = type |
324 | +__all__ = [ |
325 | + 'EC2SessionName', |
326 | + ] |
327 | + |
328 | +from datetime import datetime, timedelta |
329 | + |
330 | +from devscripts.ec2test.utils import ( |
331 | + find_datetime_string, make_datetime_string, make_random_string) |
332 | + |
333 | + |
334 | +DEFAULT_LIFETIME = timedelta(hours=6) |
335 | + |
336 | + |
337 | +class EC2SessionName(str): |
338 | + """A name for an EC2 session. |
339 | + |
340 | + This is used when naming key pairs and security groups, so it's |
341 | + useful to be unique. However, to aid garbage collection of old key |
342 | + pairs and security groups, the name contains a common element and |
343 | + an expiry timestamp. The form taken should always be: |
344 | + |
345 | + <base-name>/<expires-timestamp>/<random-data> |
346 | + |
347 | + None of the parts should contain forward-slashes, and the |
348 | + timestamp should be acceptable input to `find_datetime_string`. |
349 | + |
350 | + `EC2SessionName.make()` will generate a suitable name given a |
351 | + suitable base name. |
352 | + """ |
353 | + |
354 | + @classmethod |
355 | + def make(cls, base, expires=None): |
356 | + """Create an `EC2SessionName`. |
357 | + |
358 | + This checks that `base` does not contain a forward-slash, and |
359 | + provides some convenient functionality for `expires`: |
360 | + |
361 | + - If `expires` is None, it defaults to now (UTC) plus |
362 | + `DEFAULT_LIFETIME`. |
363 | + |
364 | + - If `expires` is a `datetime`, it is converted to a timestamp |
365 | + in the correct form. |
366 | + |
367 | + - If `expires` is a `timedelta`, it is added to now (UTC) then |
368 | + converted to a timestamp. |
369 | + |
370 | + - Otherwise `expires` is assumed to be a string, so is checked |
371 | + for the absense of forward-slashes, and that a correctly |
372 | + formed timestamp can be discovered. |
373 | + |
374 | + """ |
375 | + assert '/' not in base |
376 | + if expires is None: |
377 | + expires = DEFAULT_LIFETIME |
378 | + if isinstance(expires, timedelta): |
379 | + expires = datetime.utcnow() + expires |
380 | + if isinstance(expires, datetime): |
381 | + expires = make_datetime_string(expires) |
382 | + else: |
383 | + assert '/' not in expires |
384 | + assert find_datetime_string(expires) is not None |
385 | + rand = make_random_string(8) |
386 | + return cls("%s/%s/%s" % (base, expires, rand)) |
387 | + |
388 | + @property |
389 | + def base(self): |
390 | + parts = self.split('/') |
391 | + if len(parts) != 3: |
392 | + return None |
393 | + return parts[0] |
394 | + |
395 | + @property |
396 | + def expires(self): |
397 | + parts = self.split('/') |
398 | + if len(parts) != 3: |
399 | + return None |
400 | + return find_datetime_string(parts[1]) |
401 | + |
402 | + @property |
403 | + def rand(self): |
404 | + parts = self.split('/') |
405 | + if len(parts) != 3: |
406 | + return None |
407 | + return parts[2] |
408 | |
409 | === added directory 'lib/devscripts/ec2test/tests' |
410 | === added file 'lib/devscripts/ec2test/tests/__init__.py' |
411 | --- lib/devscripts/ec2test/tests/__init__.py 1970-01-01 00:00:00 +0000 |
412 | +++ lib/devscripts/ec2test/tests/__init__.py 2009-10-08 14:53:12 +0000 |
413 | @@ -0,0 +1,2 @@ |
414 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
415 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
416 | |
417 | === added file 'lib/devscripts/ec2test/tests/test_session.py' |
418 | --- lib/devscripts/ec2test/tests/test_session.py 1970-01-01 00:00:00 +0000 |
419 | +++ lib/devscripts/ec2test/tests/test_session.py 2009-10-08 14:53:12 +0000 |
420 | @@ -0,0 +1,69 @@ |
421 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
422 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
423 | + |
424 | +"""Test the session module.""" |
425 | + |
426 | +__metaclass__ = type |
427 | + |
428 | +import re |
429 | +import unittest |
430 | + |
431 | +from datetime import datetime, timedelta |
432 | + |
433 | +from devscripts.ec2test import session |
434 | + |
435 | + |
436 | +class TestEC2SessionName(unittest.TestCase): |
437 | + """Tests for EC2SessionName.""" |
438 | + |
439 | + def test_make(self): |
440 | + # EC2SessionName.make() is the most convenient way to create |
441 | + # valid names. |
442 | + name = session.EC2SessionName.make("fred") |
443 | + check = re.compile( |
444 | + r'^fred/\d{4}-\d{2}-\d{2}-\d{4}/[0-9a-zA-Z]{8}$').match |
445 | + self.failIf(check(name) is None, "Did not match %r" % name) |
446 | + possible_expires = [ |
447 | + None, '1986-04-26-0123', timedelta(hours=10), |
448 | + datetime(1986, 04, 26, 1, 23) |
449 | + ] |
450 | + for expires in possible_expires: |
451 | + name = session.EC2SessionName.make("fred", expires) |
452 | + self.failIf(check(name) is None, "Did not match %r" % name) |
453 | + |
454 | + def test_properties(self): |
455 | + # A valid EC2SessionName has properies to access the three |
456 | + # components of its name. |
457 | + base = "fred" |
458 | + timestamp = datetime(1986, 4, 26, 1, 23) |
459 | + timestamp_string = '1986-04-26-0123' |
460 | + rand = 'abcdef123456' |
461 | + name = session.EC2SessionName( |
462 | + "%s/%s/%s" % (base, timestamp_string, rand)) |
463 | + self.failUnlessEqual(base, name.base) |
464 | + self.failUnlessEqual(timestamp, name.expires) |
465 | + self.failUnlessEqual(rand, name.rand) |
466 | + |
467 | + def test_invalid_base(self): |
468 | + # If the given base contains a forward-slash, an |
469 | + # AssertionError should be raised. |
470 | + self.failUnlessRaises( |
471 | + AssertionError, session.EC2SessionName.make, "forward/slash") |
472 | + |
473 | + def test_invalid_timestamp(self): |
474 | + # If the given expiry timestamp contains a forward-slash, an |
475 | + # AssertionError should be raised. |
476 | + self.failUnlessRaises( |
477 | + AssertionError, session.EC2SessionName.make, "fred", "/") |
478 | + # If the given expiry timestamp does not contain a timestamp |
479 | + # in the correct form, an AssertionError should be raised. |
480 | + self.failUnlessRaises( |
481 | + AssertionError, session.EC2SessionName.make, "fred", "1986.04.26") |
482 | + |
483 | + def test_form_not_correct(self): |
484 | + # If the form of the string is not base/timestamp/rand then |
485 | + # the corresponding properties should all return None. |
486 | + broken_name = session.EC2SessionName('bob') |
487 | + self.failUnless(broken_name.base is None) |
488 | + self.failUnless(broken_name.expires is None) |
489 | + self.failUnless(broken_name.rand is None) |
490 | |
491 | === added file 'lib/devscripts/ec2test/tests/test_utils.py' |
492 | --- lib/devscripts/ec2test/tests/test_utils.py 1970-01-01 00:00:00 +0000 |
493 | +++ lib/devscripts/ec2test/tests/test_utils.py 2009-10-08 14:53:12 +0000 |
494 | @@ -0,0 +1,61 @@ |
495 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
496 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
497 | + |
498 | +"""Test the utils module.""" |
499 | + |
500 | +__metaclass__ = type |
501 | + |
502 | +import unittest |
503 | + |
504 | +from datetime import datetime |
505 | + |
506 | +from devscripts.ec2test import utils |
507 | + |
508 | + |
509 | +class TestDateTimeUtils(unittest.TestCase): |
510 | + """Tests for date/time related utilities.""" |
511 | + |
512 | + example_date = datetime(1986, 4, 26, 1, 23) |
513 | + example_date_string = '1986-04-26-0123' |
514 | + example_date_text = ( |
515 | + 'blah blah foo blah 23545 646 ' + |
516 | + example_date_string + ' 435 blah') |
517 | + |
518 | + def test_make_datetime_string(self): |
519 | + self.failUnlessEqual( |
520 | + self.example_date_string, |
521 | + utils.make_datetime_string(self.example_date)) |
522 | + |
523 | + def test_find_datetime_string(self): |
524 | + self.failUnlessEqual( |
525 | + self.example_date, |
526 | + utils.find_datetime_string(self.example_date_string)) |
527 | + self.failUnlessEqual( |
528 | + self.example_date, |
529 | + utils.find_datetime_string(self.example_date_text)) |
530 | + |
531 | + |
532 | +class TestRandomUtils(unittest.TestCase): |
533 | + """Tests for randomness related utilities.""" |
534 | + |
535 | + hex_chars = frozenset('0123456789abcdefABCDEF') |
536 | + |
537 | + def test_make_random_string(self): |
538 | + rand_a = utils.make_random_string() |
539 | + rand_b = utils.make_random_string() |
540 | + self.failIfEqual(rand_a, rand_b) |
541 | + self.failUnlessEqual(32, len(rand_a)) |
542 | + self.failUnlessEqual(32, len(rand_b)) |
543 | + self.failUnless(self.hex_chars.issuperset(rand_a)) |
544 | + self.failUnless(self.hex_chars.issuperset(rand_b)) |
545 | + |
546 | + def test_make_random_string_with_length(self): |
547 | + for length in (8, 16, 64): |
548 | + rand = utils.make_random_string(length) |
549 | + self.failUnlessEqual(length, len(rand)) |
550 | + self.failUnless(self.hex_chars.issuperset(rand)) |
551 | + |
552 | + def test_make_random_string_with_bad_length(self): |
553 | + # length must be a multiple of 2. |
554 | + self.failUnlessRaises( |
555 | + AssertionError, utils.make_random_string, 15) |
556 | |
557 | === added file 'lib/devscripts/ec2test/utils.py' |
558 | --- lib/devscripts/ec2test/utils.py 1970-01-01 00:00:00 +0000 |
559 | +++ lib/devscripts/ec2test/utils.py 2009-10-08 14:53:12 +0000 |
560 | @@ -0,0 +1,55 @@ |
561 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
562 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
563 | + |
564 | +"""General useful stuff.""" |
565 | + |
566 | +__metaclass__ = type |
567 | +__all__ = [ |
568 | + 'find_datetime_string', |
569 | + 'make_datetime_string', |
570 | + 'make_random_string', |
571 | + ] |
572 | + |
573 | + |
574 | +import binascii |
575 | +import datetime |
576 | +import os |
577 | +import re |
578 | + |
579 | + |
580 | +def make_datetime_string(when=None): |
581 | + """Generate a simple formatted date and time string. |
582 | + |
583 | + This is intended to be embedded in text to be later found by |
584 | + `find_datetime_string`. |
585 | + """ |
586 | + if when is None: |
587 | + when = datetime.datetime.utcnow() |
588 | + return when.strftime('%Y-%m-%d-%H%M') |
589 | + |
590 | + |
591 | +re_find_datetime = re.compile( |
592 | + r'(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})') |
593 | + |
594 | +def find_datetime_string(text): |
595 | + """Search for a simple date and time in arbitrary text. |
596 | + |
597 | + The format searched for is %Y-%m-%d-%H%M - the same as produced by |
598 | + `make_datetime_string`. |
599 | + """ |
600 | + match = re_find_datetime.search(text) |
601 | + if match is None: |
602 | + return None |
603 | + else: |
604 | + return datetime.datetime( |
605 | + *(int(part) for part in match.groups())) |
606 | + |
607 | + |
608 | +def make_random_string(length=32): |
609 | + """Return a simple random UUID. |
610 | + |
611 | + The uuid module is only available in Python 2.5 and above, but a |
612 | + simple non-RFC-compliant hack here is sufficient. |
613 | + """ |
614 | + assert length % 2 == 0, "length must be a multiple of 2" |
615 | + return binascii.hexlify(os.urandom(length/2)) |
616 | |
617 | === modified file 'setup.py' |
618 | --- setup.py 2009-08-17 19:16:23 +0000 |
619 | +++ setup.py 2009-10-08 14:53:12 +0000 |
620 | @@ -41,6 +41,7 @@ |
621 | 'mechanize', |
622 | 'mocker', |
623 | 'oauth', |
624 | + 'paramiko', |
625 | 'python-openid', |
626 | 'pytz', |
627 | # This appears to be a broken indirect dependency from zope.security: |
628 | |
629 | === modified file 'versions.cfg' |
630 | --- versions.cfg 2009-10-01 16:38:03 +0000 |
631 | +++ versions.cfg 2009-10-08 14:53:12 +0000 |
632 | @@ -35,8 +35,10 @@ |
633 | mocker = 0.10.1 |
634 | mozrunner = 1.3.4 |
635 | oauth = 1.0 |
636 | +paramiko = 1.7.4 |
637 | Paste = 1.7.2 |
638 | PasteDeploy = 1.3.3 |
639 | +pycrypto = 2.0.1 |
640 | python-openid = 2.2.1 |
641 | pytz = 2009l |
642 | RestrictedPython = 3.4.2 |
This is a resubmission of an earlier branch:
https:/ /code.edge. launchpad. net/~allenap/ launchpad/ ec2-test- race-bug- 422433/ +merge/ 12778
It got merged, but had to be reverted because of failures in buildbot. This branch restores it, with some additional fixes to make it pass in buildbot:
http:// pastebin. ubuntu. com/288542/
No need to review the whole branch :)
This depends on https:/ /code.edge. launchpad. net/~allenap/ lp-source- dependencies/ ec2-test- race-bug- 422433- the-revenge/ +merge/ 13063.