blob: 6c5134679c509d330a3196eecd711279644f1fb0 [file] [log] [blame]
Attila Fazekasa23f5002012-10-23 19:32:45 +02001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2012 OpenStack, LLC
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import unittest2 as unittest
19import nose
20import tempest.tests.boto
21from tempest.exceptions import TearDownException
22from tempest.tests.boto.utils.wait import state_wait, wait_no_exception
23from tempest.tests.boto.utils.wait import re_search_wait, wait_exception
24import boto
25from boto.s3.key import Key
26from boto.s3.bucket import Bucket
27from boto.exception import BotoServerError
28from contextlib import closing
29import re
30import logging
31import time
32
33LOG = logging.getLogger(__name__)
34
35
36class BotoExceptionMatcher(object):
37 STATUS_RE = r'[45]\d\d'
38 CODE_RE = '.*' # regexp makes sense in group match
39
40 def match(self, exc):
41 if not isinstance(exc, BotoServerError):
42 return "%r not an BotoServerError instance" % exc
43 LOG.info("Status: %s , error_code: %s", exc.status, exc.error_code)
44 if re.match(self.STATUS_RE, str(exc.status)) is None:
45 return ("Status code (%s) does not match"
46 "the expected re pattern \"%s\""
47 % (exc.status, self.STATUS_RE))
48 if re.match(self.CODE_RE, str(exc.error_code)) is None:
49 return ("Error code (%s) does not match" +
50 "the expected re pattern \"%s\"") %\
51 (exc.error_code, self.CODE_RE)
52
53
54class ClientError(BotoExceptionMatcher):
55 STATUS_RE = r'4\d\d'
56
57
58class ServerError(BotoExceptionMatcher):
59 STATUS_RE = r'5\d\d'
60
61
62def _add_matcher_class(error_cls, error_data, base=BotoExceptionMatcher):
63 """
64 Usable for adding an ExceptionMatcher(s) into the exception tree.
65 The not leaf elements does wildcard match
66 """
67 # in error_code just literal and '.' characters expected
68 if not isinstance(error_data, basestring):
69 (error_code, status_code) = map(str, error_data)
70 else:
71 status_code = None
72 error_code = error_data
73 parts = error_code.split('.')
74 basematch = ""
75 num_parts = len(parts)
76 max_index = num_parts - 1
77 add_cls = error_cls
78 for i_part in xrange(num_parts):
79 part = parts[i_part]
80 leaf = i_part == max_index
81 if not leaf:
82 match = basematch + part + "[.].*"
83 else:
84 match = basematch + part
85
86 basematch += part + "[.]"
87 if not hasattr(add_cls, part):
88 cls_dict = {"CODE_RE": match}
89 if leaf and status_code is not None:
90 cls_dict["STATUS_RE"] = status_code
91 cls = type(part, (base, ), cls_dict)
92 setattr(add_cls, part, cls())
93 add_cls = cls
94 elif leaf:
95 raise LookupError("Tries to redefine an error code \"%s\"" % part)
96 else:
97 add_cls = getattr(add_cls, part)
98
99
100#TODO(afazekas): classmethod handling
101def friendly_function_name_simple(call_able):
102 name = ""
103 if hasattr(call_able, "im_class"):
104 name += call_able.im_class.__name__ + "."
105 name += call_able.__name__
106 return name
107
108
109def friendly_function_call_str(call_able, *args, **kwargs):
110 string = friendly_function_name_simple(call_able)
111 string += "(" + ", ".join(map(str, args))
112 if len(kwargs):
113 if len(args):
114 string += ", "
115 string += ", ".join("=".join(map(str, (key, value)))
116 for (key, value) in kwargs.items())
117 return string + ")"
118
119
120class BotoTestCase(unittest.TestCase):
121 """Recommended to use as base class for boto related test"""
122 @classmethod
123 def setUpClass(cls):
124 # The trash contains cleanup functions and paramaters in tuples
125 # (function, *args, **kwargs)
126 cls._resource_trash_bin = {}
127 cls._sequence = -1
128 if (hasattr(cls, "EC2") and
129 tempest.tests.boto.EC2_CAN_CONNECT_ERROR is not None):
130 raise nose.SkipTest("EC2 " + cls.__name__ + ": " +
131 tempest.tests.boto.EC2_CAN_CONNECT_ERROR)
132 if (hasattr(cls, "S3") and
133 tempest.tests.boto.S3_CAN_CONNECT_ERROR is not None):
134 raise nose.SkipTest("S3 " + cls.__name__ + ": " +
135 tempest.tests.boto.S3_CAN_CONNECT_ERROR)
136
137 @classmethod
138 def addResourceCleanUp(cls, function, *args, **kwargs):
139 """Adds CleanUp callable, used by tearDownClass.
140 Recommended to a use (deep)copy on the mutable args"""
141 cls._sequence = cls._sequence + 1
142 cls._resource_trash_bin[cls._sequence] = (function, args, kwargs)
143 return cls._sequence
144
145 @classmethod
146 def cancelResourceCleanUp(cls, key):
147 """Cancel Clean up request"""
148 del cls._resource_trash_bin[key]
149
150 #TODO(afazekas): Add "with" context handling
151 def assertBotoError(self, excMatcher, callableObj,
152 *args, **kwargs):
153 """Example usage:
154 self.assertBotoError(self.ec2_error_code.client.
155 InvalidKeyPair.Duplicate,
156 self.client.create_keypair,
157 key_name)"""
158 try:
159 callableObj(*args, **kwargs)
160 except BotoServerError as exc:
161 error_msg = excMatcher.match(exc)
162 if error_msg is not None:
163 raise self.failureException, error_msg
164 else:
165 raise self.failureException, "BotoServerError not raised"
166
167 @classmethod
168 def tearDownClass(cls):
169 """ Calls the callables added by addResourceCleanUp,
170 when you overwire this function dont't forget to call this too"""
171 fail_count = 0
172 trash_keys = sorted(cls._resource_trash_bin, reverse=True)
173 for key in trash_keys:
174 (function, pos_args, kw_args) = cls._resource_trash_bin[key]
175 try:
176 LOG.debug("Cleaning up: %s" %
177 friendly_function_call_str(function, *pos_args,
178 **kw_args))
179 function(*pos_args, **kw_args)
180 except BaseException as exc:
181 fail_count += 1
182 LOG.exception(exc)
183 finally:
184 del cls._resource_trash_bin[key]
185 if fail_count:
186 raise TearDownException(num=fail_count)
187
188 ec2_error_code = BotoExceptionMatcher()
189 # InsufficientInstanceCapacity can be both server and client error
190 ec2_error_code.server = ServerError()
191 ec2_error_code.client = ClientError()
192 s3_error_code = BotoExceptionMatcher()
193 s3_error_code.server = ServerError()
194 s3_error_code.client = ClientError()
195 valid_image_state = set(('available', 'pending', 'failed'))
196 valid_instance_state = set(('pending', 'running', 'shutting-down',
197 'terminated', 'stopping', 'stopped'))
198 valid_volume_status = set(('creating', 'available', 'in-use',
199 'deleting', 'deleted', 'error'))
200 valid_snapshot_status = set(('pending', 'completed', 'error'))
201
202 #TODO(afazekas): object base version for resurces supports update
203 def waitImageState(self, lfunction, wait_for):
204 state = state_wait(lfunction, wait_for, self.valid_image_state)
205 self.assertIn(state, self.valid_image_state)
206 return state
207
208 def waitInstanceState(self, lfunction, wait_for):
209 state = state_wait(lfunction, wait_for, self.valid_instance_state)
210 self.assertIn(state, self.valid_instance_state)
211 return state
212
213 def waitVolumeStatus(self, lfunction, wait_for):
214 state = state_wait(lfunction, wait_for, self.valid_volume_status)
215 self.assertIn(state, self.valid_volume_status)
216 return state
217
218 def waitSnapshotStatus(self, lfunction, wait_for):
219 state = state_wait(lfunction, wait_for, self.valid_snapshot_status)
220 self.assertIn(state, self.valid_snapshot_status)
221 return state
222
223 def assertImageStateWait(self, lfunction, wait_for):
224 state = self.waitImageState(lfunction, wait_for)
225 self.assertIn(state, wait_for)
226
227 def assertInstanceStateWait(self, lfunction, wait_for):
228 state = self.waitInstanceState(lfunction, wait_for)
229 self.assertIn(state, wait_for)
230
231 def assertVolumeStatusWait(self, lfunction, wait_for):
232 state = self.waitVolumeStatus(lfunction, wait_for)
233 self.assertIn(state, wait_for)
234
235 def assertSnapshotStatusWait(self, lfunction, wait_for):
236 state = self.waitSnapshotStatus(lfunction, wait_for)
237 self.assertIn(state, wait_for)
238
239 def assertAddressDissasociatedWait(self, address):
240
241 def _disassociate():
242 cli = self.ec2_client
243 addresses = cli.get_all_addresses(addresses=(address.public_ip,))
244 if len(addresses) != 1:
245 return "INVALID"
246 if addresses[0].instance_id:
247 LOG.info("%s associated to %s",
248 address.public_ip,
249 addresses[0].instance_id)
250 return "ASSOCIATED"
251 return "DISASSOCIATED"
252
253 state = state_wait(_disassociate, "DISASSOCIATED",
254 set(("ASSOCIATED", "DISASSOCIATED")))
255 self.assertEqual(state, "DISASSOCIATED")
256
257 def assertAddressReleasedWait(self, address):
258
259 def _address_delete():
260 #NOTE(afazekas): the filter gives back IP
261 # even if it is not associated to my tenant
262 if (address.public_ip not in map(lambda a: a.public_ip,
263 self.ec2_client.get_all_addresses())):
264 return "DELETED"
265 return "NOTDELETED"
266
267 state = state_wait(_address_delete, "DELETED")
268 self.assertEqual(state, "DELETED")
269
270 def assertReSearch(self, regexp, string):
271 if re.search(regexp, string) is None:
272 raise self.failureException("regexp: '%s' not found in '%s'" %
273 (regexp, string))
274
275 def assertNotReSearch(self, regexp, string):
276 if re.search(regexp, string) is not None:
277 raise self.failureException("regexp: '%s' found in '%s'" %
278 (regexp, string))
279
280 def assertReMatch(self, regexp, string):
281 if re.match(regexp, string) is None:
282 raise self.failureException("regexp: '%s' not matches on '%s'" %
283 (regexp, string))
284
285 def assertNotReMatch(self, regexp, string):
286 if re.match(regexp, string) is not None:
287 raise self.failureException("regexp: '%s' matches on '%s'" %
288 (regexp, string))
289
290 @classmethod
291 def destroy_bucket(cls, connection_data, bucket):
292 """Destroys the bucket and its content, just for teardown"""
293 exc_num = 0
294 try:
295 with closing(boto.connect_s3(**connection_data)) as conn:
296 if isinstance(bucket, basestring):
297 bucket = conn.lookup(bucket)
298 assert isinstance(bucket, Bucket)
299 for obj in bucket.list():
300 try:
301 bucket.delete_key(obj.key)
302 obj.close()
303 except BaseException as exc:
304 LOG.exception(exc)
305 exc_num += 1
306 conn.delete_bucket(bucket)
307 except BaseException as exc:
308 LOG.exception(exc)
309 exc_num += 1
310 if exc_num:
311 raise TearDownException(num=exc_num)
312
313 @classmethod
314 def destroy_reservation(cls, reservation):
315 """Terminate instances in a reservation, just for teardown"""
316 exc_num = 0
317
318 def _instance_state():
319 try:
320 instance.update(validate=True)
321 except ValueError:
322 return "terminated"
323 return instance.state
324
325 for instance in reservation.instances:
326 try:
327 instance.terminate()
328 re_search_wait(_instance_state, "terminated")
329 except BaseException as exc:
330 LOG.exception(exc)
331 exc_num += 1
332 if exc_num:
333 raise TearDownException(num=exc_num)
334
335 #NOTE(afazekas): The incorrect ErrorCodes makes very, very difficult
336 # to write better teardown
337
338 @classmethod
339 def destroy_security_group_wait(cls, group):
340 """Delete group.
341 Use just for teardown!
342 """
343 #NOTE(afazekas): should wait/try until all related instance terminates
344 #2. looks like it is locked even if the instance not listed
345 time.sleep(1)
346 group.delete()
347
348 @classmethod
349 def destroy_volume_wait(cls, volume):
350 """Delete volume, tryies to detach first.
351 Use just for teardown!
352 """
353 exc_num = 0
354 snaps = volume.snapshots()
355 if len(snaps):
356 LOG.critical("%s Volume has %s snapshot(s)", volume.id,
357 map(snps.id, snaps))
358
359 #Note(afazekas): detaching/attching not valid EC2 status
360 def _volume_state():
361 volume.update(validate=True)
362 try:
363 if volume.status != "available":
364 volume.detach(force=True)
365 except BaseException as exc:
366 LOG.exception(exc)
367 #exc_num += 1 "nonlocal" not in python2
368 return volume.status
369
370 try:
371 re_search_wait(_volume_state, "available") # not validates status
372 LOG.info(_volume_state())
373 volume.delete()
374 except BaseException as exc:
375 LOG.exception(exc)
376 exc_num += 1
377 if exc_num:
378 raise TearDownException(num=exc_num)
379
380 @classmethod
381 def destroy_snapshot_wait(cls, snapshot):
382 """delete snaphot, wait until not exists"""
383 snapshot.delete()
384
385 def _update():
386 snapshot.update(validate=True)
387
388 wait_exception(_update)
389
390
391# you can specify tuples if you want to specify the status pattern
392for code in ('AddressLimitExceeded', 'AttachmentLimitExceeded', 'AuthFailure',
393 'Blocked', 'CustomerGatewayLimitExceeded', 'DependencyViolation',
394 'DiskImageSizeTooLarge', 'FilterLimitExceeded',
395 'Gateway.NotAttached', 'IdempotentParameterMismatch',
396 'IncorrectInstanceState', 'IncorrectState',
397 'InstanceLimitExceeded', 'InsufficientInstanceCapacity',
398 'InsufficientReservedInstancesCapacity',
399 'InternetGatewayLimitExceeded', 'InvalidAMIAttributeItemValue',
400 'InvalidAMIID.Malformed', 'InvalidAMIID.NotFound',
401 'InvalidAMIID.Unavailable', 'InvalidAssociationID.NotFound',
402 'InvalidAttachment.NotFound', 'InvalidConversionTaskId',
403 'InvalidCustomerGateway.DuplicateIpAddress',
404 'InvalidCustomerGatewayID.NotFound', 'InvalidDevice.InUse',
405 'InvalidDhcpOptionsID.NotFound', 'InvalidFormat',
406 'InvalidFilter', 'InvalidGatewayID.NotFound',
407 'InvalidGroup.Duplicate', 'InvalidGroupId.Malformed',
408 'InvalidGroup.InUse', 'InvalidGroup.NotFound',
409 'InvalidGroup.Reserved', 'InvalidInstanceID.Malformed',
410 'InvalidInstanceID.NotFound',
411 'InvalidInternetGatewayID.NotFound', 'InvalidIPAddress.InUse',
412 'InvalidKeyPair.Duplicate', 'InvalidKeyPair.Format',
413 'InvalidKeyPair.NotFound', 'InvalidManifest',
414 'InvalidNetworkAclEntry.NotFound',
415 'InvalidNetworkAclID.NotFound', 'InvalidParameterCombination',
416 'InvalidParameterValue', 'InvalidPermission.Duplicate',
417 'InvalidPermission.Malformed', 'InvalidReservationID.Malformed',
418 'InvalidReservationID.NotFound', 'InvalidRoute.NotFound',
419 'InvalidRouteTableID.NotFound',
420 'InvalidSecurity.RequestHasExpired',
421 'InvalidSnapshotID.Malformed', 'InvalidSnapshot.NotFound',
422 'InvalidUserID.Malformed', 'InvalidReservedInstancesId',
423 'InvalidReservedInstancesOfferingId',
424 'InvalidSubnetID.NotFound', 'InvalidVolumeID.Duplicate',
425 'InvalidVolumeID.Malformed', 'InvalidVolumeID.ZoneMismatch',
426 'InvalidVolume.NotFound', 'InvalidVpcID.NotFound',
427 'InvalidVpnConnectionID.NotFound',
428 'InvalidVpnGatewayID.NotFound',
429 'InvalidZone.NotFound', 'LegacySecurityGroup',
430 'MissingParameter', 'NetworkAclEntryAlreadyExists',
431 'NetworkAclEntryLimitExceeded', 'NetworkAclLimitExceeded',
432 'NonEBSInstance', 'PendingSnapshotLimitExceeded',
433 'PendingVerification', 'OptInRequired', 'RequestLimitExceeded',
434 'ReservedInstancesLimitExceeded', 'Resource.AlreadyAssociated',
435 'ResourceLimitExceeded', 'RouteAlreadyExists',
436 'RouteLimitExceeded', 'RouteTableLimitExceeded',
437 'RulesPerSecurityGroupLimitExceeded',
438 'SecurityGroupLimitExceeded',
439 'SecurityGroupsPerInstanceLimitExceeded',
440 'SnapshotLimitExceeded', 'SubnetLimitExceeded',
441 'UnknownParameter', 'UnsupportedOperation',
442 'VolumeLimitExceeded', 'VpcLimitExceeded',
443 'VpnConnectionLimitExceeded',
444 'VpnGatewayAttachmentLimitExceeded', 'VpnGatewayLimitExceeded'):
445 _add_matcher_class(BotoTestCase.ec2_error_code.client,
446 code, base=ClientError)
447
448for code in ('InsufficientAddressCapacity', 'InsufficientInstanceCapacity',
449 'InsufficientReservedInstanceCapacity', 'InternalError',
450 'Unavailable'):
451 _add_matcher_class(BotoTestCase.ec2_error_code.server,
452 code, base=ServerError)
453
454
455for code in (('AccessDenied', 403),
456 ('AccountProblem', 403),
457 ('AmbiguousGrantByEmailAddress', 400),
458 ('BadDigest', 400),
459 ('BucketAlreadyExists', 409),
460 ('BucketAlreadyOwnedByYou', 409),
461 ('BucketNotEmpty', 409),
462 ('CredentialsNotSupported', 400),
463 ('CrossLocationLoggingProhibited', 403),
464 ('EntityTooSmall', 400),
465 ('EntityTooLarge', 400),
466 ('ExpiredToken', 400),
467 ('IllegalVersioningConfigurationException', 400),
468 ('IncompleteBody', 400),
469 ('IncorrectNumberOfFilesInPostRequest', 400),
470 ('InlineDataTooLarge', 400),
471 ('InvalidAccessKeyId', 403),
472 'InvalidAddressingHeader',
473 ('InvalidArgument', 400),
474 ('InvalidBucketName', 400),
475 ('InvalidBucketState', 409),
476 ('InvalidDigest', 400),
477 ('InvalidLocationConstraint', 400),
478 ('InvalidPart', 400),
479 ('InvalidPartOrder', 400),
480 ('InvalidPayer', 403),
481 ('InvalidPolicyDocument', 400),
482 ('InvalidRange', 416),
483 ('InvalidRequest', 400),
484 ('InvalidSecurity', 403),
485 ('InvalidSOAPRequest', 400),
486 ('InvalidStorageClass', 400),
487 ('InvalidTargetBucketForLogging', 400),
488 ('InvalidToken', 400),
489 ('InvalidURI', 400),
490 ('KeyTooLong', 400),
491 ('MalformedACLError', 400),
492 ('MalformedPOSTRequest', 400),
493 ('MalformedXML', 400),
494 ('MaxMessageLengthExceeded', 400),
495 ('MaxPostPreDataLengthExceededError', 400),
496 ('MetadataTooLarge', 400),
497 ('MethodNotAllowed', 405),
498 ('MissingAttachment'),
499 ('MissingContentLength', 411),
500 ('MissingRequestBodyError', 400),
501 ('MissingSecurityElement', 400),
502 ('MissingSecurityHeader', 400),
503 ('NoLoggingStatusForKey', 400),
504 ('NoSuchBucket', 404),
505 ('NoSuchKey', 404),
506 ('NoSuchLifecycleConfiguration', 404),
507 ('NoSuchUpload', 404),
508 ('NoSuchVersion', 404),
509 ('NotSignedUp', 403),
510 ('NotSuchBucketPolicy', 404),
511 ('OperationAborted', 409),
512 ('PermanentRedirect', 301),
513 ('PreconditionFailed', 412),
514 ('Redirect', 307),
515 ('RequestIsNotMultiPartContent', 400),
516 ('RequestTimeout', 400),
517 ('RequestTimeTooSkewed', 403),
518 ('RequestTorrentOfBucketError', 400),
519 ('SignatureDoesNotMatch', 403),
520 ('TemporaryRedirect', 307),
521 ('TokenRefreshRequired', 400),
522 ('TooManyBuckets', 400),
523 ('UnexpectedContent', 400),
524 ('UnresolvableGrantByEmailAddress', 400),
525 ('UserKeyMustBeSpecified', 400)):
526 _add_matcher_class(BotoTestCase.s3_error_code.client,
527 code, base=ClientError)
528
529
530for code in (('InternalError', 500),
531 ('NotImplemented', 501),
532 ('ServiceUnavailable', 503),
533 ('SlowDown', 503)):
534 _add_matcher_class(BotoTestCase.s3_error_code.server,
535 code, base=ServerError)