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