OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 # Copyright 2013 Google Inc. All Rights Reserved. |
| 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with the License. |
| 6 # You may obtain a copy of the License at |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. |
| 15 """Contains gsutil base integration test case class.""" |
| 16 |
| 17 from __future__ import absolute_import |
| 18 |
| 19 from contextlib import contextmanager |
| 20 import cStringIO |
| 21 import locale |
| 22 import logging |
| 23 import os |
| 24 import subprocess |
| 25 import sys |
| 26 import tempfile |
| 27 |
| 28 import boto |
| 29 from boto.exception import StorageResponseError |
| 30 from boto.s3.deletemarker import DeleteMarker |
| 31 from boto.storage_uri import BucketStorageUri |
| 32 |
| 33 import gslib |
| 34 from gslib.gcs_json_api import GcsJsonApi |
| 35 from gslib.hashing_helper import Base64ToHexHash |
| 36 from gslib.project_id import GOOG_PROJ_ID_HDR |
| 37 from gslib.project_id import PopulateProjectId |
| 38 from gslib.tests.testcase import base |
| 39 import gslib.tests.util as util |
| 40 from gslib.tests.util import ObjectToURI as suri |
| 41 from gslib.tests.util import RUN_S3_TESTS |
| 42 from gslib.tests.util import SetBotoConfigFileForTest |
| 43 from gslib.tests.util import SetBotoConfigForTest |
| 44 from gslib.tests.util import SetEnvironmentForTest |
| 45 from gslib.tests.util import unittest |
| 46 import gslib.third_party.storage_apitools.storage_v1_messages as apitools_messag
es |
| 47 from gslib.util import IS_WINDOWS |
| 48 from gslib.util import Retry |
| 49 from gslib.util import UTF8 |
| 50 |
| 51 |
| 52 LOGGER = logging.getLogger('integration-test') |
| 53 |
| 54 # Contents of boto config file that will tell gsutil not to override the real |
| 55 # error message with a warning about anonymous access if no credentials are |
| 56 # provided in the config file. Also, because we retry 401s, reduce the number |
| 57 # of retries so we don't go through a long exponential backoff in tests. |
| 58 BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING = """ |
| 59 [Boto] |
| 60 num_retries = 2 |
| 61 [Tests] |
| 62 bypass_anonymous_access_warning = True |
| 63 """ |
| 64 |
| 65 |
| 66 def SkipForGS(reason): |
| 67 if not RUN_S3_TESTS: |
| 68 return unittest.skip(reason) |
| 69 else: |
| 70 return lambda func: func |
| 71 |
| 72 |
| 73 def SkipForS3(reason): |
| 74 if RUN_S3_TESTS: |
| 75 return unittest.skip(reason) |
| 76 else: |
| 77 return lambda func: func |
| 78 |
| 79 |
| 80 # TODO: Right now, most tests use the XML API. Instead, they should respect |
| 81 # prefer_api in the same way that commands do. |
| 82 @unittest.skipUnless(util.RUN_INTEGRATION_TESTS, |
| 83 'Not running integration tests.') |
| 84 class GsUtilIntegrationTestCase(base.GsUtilTestCase): |
| 85 """Base class for gsutil integration tests.""" |
| 86 GROUP_TEST_ADDRESS = 'gs-discussion@googlegroups.com' |
| 87 GROUP_TEST_ID = ( |
| 88 '00b4903a97d097895ab58ef505d535916a712215b79c3e54932c2eb502ad97f5') |
| 89 USER_TEST_ADDRESS = 'gs-team@google.com' |
| 90 USER_TEST_ID = ( |
| 91 '00b4903a9703325c6bfc98992d72e75600387a64b3b6bee9ef74613ef8842080') |
| 92 DOMAIN_TEST = 'google.com' |
| 93 # No one can create this bucket without owning the gmail.com domain, and we |
| 94 # won't create this bucket, so it shouldn't exist. |
| 95 # It would be nice to use google.com here but JSON API disallows |
| 96 # 'google' in resource IDs. |
| 97 nonexistent_bucket_name = 'nonexistent-bucket-foobar.gmail.com' |
| 98 |
| 99 def setUp(self): |
| 100 """Creates base configuration for integration tests.""" |
| 101 super(GsUtilIntegrationTestCase, self).setUp() |
| 102 self.bucket_uris = [] |
| 103 |
| 104 # Set up API version and project ID handler. |
| 105 self.api_version = boto.config.get_value( |
| 106 'GSUtil', 'default_api_version', '1') |
| 107 |
| 108 # Instantiate a JSON API for use by the current integration test. |
| 109 self.json_api = GcsJsonApi(BucketStorageUri, logging.getLogger(), |
| 110 'gs') |
| 111 |
| 112 if util.RUN_S3_TESTS: |
| 113 self.nonexistent_bucket_name = ( |
| 114 'nonexistentbucket-asf801rj3r9as90mfnnkjxpo02') |
| 115 |
| 116 # Retry with an exponential backoff if a server error is received. This |
| 117 # ensures that we try *really* hard to clean up after ourselves. |
| 118 # TODO: As long as we're still using boto to do the teardown, |
| 119 # we decorate with boto exceptions. Eventually this should be migrated |
| 120 # to CloudApi exceptions. |
| 121 @Retry(StorageResponseError, tries=7, timeout_secs=1) |
| 122 def tearDown(self): |
| 123 super(GsUtilIntegrationTestCase, self).tearDown() |
| 124 |
| 125 while self.bucket_uris: |
| 126 bucket_uri = self.bucket_uris[-1] |
| 127 try: |
| 128 bucket_list = self._ListBucket(bucket_uri) |
| 129 except StorageResponseError, e: |
| 130 # This can happen for tests of rm -r command, which for bucket-only |
| 131 # URIs delete the bucket at the end. |
| 132 if e.status == 404: |
| 133 self.bucket_uris.pop() |
| 134 continue |
| 135 else: |
| 136 raise |
| 137 while bucket_list: |
| 138 error = None |
| 139 for k in bucket_list: |
| 140 try: |
| 141 if isinstance(k, DeleteMarker): |
| 142 bucket_uri.get_bucket().delete_key(k.name, |
| 143 version_id=k.version_id) |
| 144 else: |
| 145 k.delete() |
| 146 except StorageResponseError, e: |
| 147 # This could happen if objects that have already been deleted are |
| 148 # still showing up in the listing due to eventual consistency. In |
| 149 # that case, we continue on until we've tried to deleted every |
| 150 # object in the listing before raising the error on which to retry. |
| 151 if e.status == 404: |
| 152 error = e |
| 153 else: |
| 154 raise |
| 155 if error: |
| 156 raise error # pylint: disable=raising-bad-type |
| 157 bucket_list = self._ListBucket(bucket_uri) |
| 158 bucket_uri.delete_bucket() |
| 159 self.bucket_uris.pop() |
| 160 |
| 161 def _ListBucket(self, bucket_uri): |
| 162 if bucket_uri.scheme == 's3': |
| 163 # storage_uri will omit delete markers from bucket listings, but |
| 164 # these must be deleted before we can remove an S3 bucket. |
| 165 return list(v for v in bucket_uri.get_bucket().list_versions()) |
| 166 return list(bucket_uri.list_bucket(all_versions=True)) |
| 167 |
| 168 def AssertNObjectsInBucket(self, bucket_uri, num_objects, versioned=False): |
| 169 """Checks (with retries) that 'ls bucket_uri/**' returns num_objects. |
| 170 |
| 171 This is a common test pattern to deal with eventual listing consistency for |
| 172 tests that rely on a set of objects to be listed. |
| 173 |
| 174 Args: |
| 175 bucket_uri: storage_uri for the bucket. |
| 176 num_objects: number of objects expected in the bucket. |
| 177 versioned: If True, perform a versioned listing. |
| 178 |
| 179 Raises: |
| 180 AssertionError if number of objects does not match expected value. |
| 181 |
| 182 Returns: |
| 183 Listing split across lines. |
| 184 """ |
| 185 # Use @Retry as hedge against bucket listing eventual consistency. |
| 186 @Retry(AssertionError, tries=5, timeout_secs=1) |
| 187 def _Check1(): |
| 188 command = ['ls', '-a'] if versioned else ['ls'] |
| 189 b_uri = [suri(bucket_uri) + '/**'] if num_objects else [suri(bucket_uri)] |
| 190 listing = self.RunGsUtil(command + b_uri, return_stdout=True).split('\n') |
| 191 # num_objects + one trailing newline. |
| 192 self.assertEquals(len(listing), num_objects + 1) |
| 193 return listing |
| 194 return _Check1() |
| 195 |
| 196 def CreateBucket(self, bucket_name=None, test_objects=0, storage_class=None, |
| 197 provider=None, prefer_json_api=False): |
| 198 """Creates a test bucket. |
| 199 |
| 200 The bucket and all of its contents will be deleted after the test. |
| 201 |
| 202 Args: |
| 203 bucket_name: Create the bucket with this name. If not provided, a |
| 204 temporary test bucket name is constructed. |
| 205 test_objects: The number of objects that should be placed in the bucket. |
| 206 Defaults to 0. |
| 207 storage_class: storage class to use. If not provided we us standard. |
| 208 provider: Provider to use - either "gs" (the default) or "s3". |
| 209 prefer_json_api: If true, use the JSON creation functions where possible. |
| 210 |
| 211 Returns: |
| 212 StorageUri for the created bucket. |
| 213 """ |
| 214 if not provider: |
| 215 provider = self.default_provider |
| 216 |
| 217 if prefer_json_api and provider == 'gs': |
| 218 json_bucket = self.CreateBucketJson(bucket_name=bucket_name, |
| 219 test_objects=test_objects, |
| 220 storage_class=storage_class) |
| 221 bucket_uri = boto.storage_uri( |
| 222 'gs://%s' % json_bucket.name.encode(UTF8).lower(), |
| 223 suppress_consec_slashes=False) |
| 224 self.bucket_uris.append(bucket_uri) |
| 225 return bucket_uri |
| 226 |
| 227 bucket_name = bucket_name or self.MakeTempName('bucket') |
| 228 |
| 229 bucket_uri = boto.storage_uri('%s://%s' % (provider, bucket_name.lower()), |
| 230 suppress_consec_slashes=False) |
| 231 |
| 232 if provider == 'gs': |
| 233 # Apply API version and project ID headers if necessary. |
| 234 headers = {'x-goog-api-version': self.api_version} |
| 235 headers[GOOG_PROJ_ID_HDR] = PopulateProjectId() |
| 236 else: |
| 237 headers = {} |
| 238 |
| 239 # Parallel tests can easily run into bucket creation quotas. |
| 240 # Retry with exponential backoff so that we create them as fast as we |
| 241 # reasonably can. |
| 242 @Retry(StorageResponseError, tries=7, timeout_secs=1) |
| 243 def _CreateBucketWithExponentialBackoff(): |
| 244 bucket_uri.create_bucket(storage_class=storage_class, headers=headers) |
| 245 |
| 246 _CreateBucketWithExponentialBackoff() |
| 247 self.bucket_uris.append(bucket_uri) |
| 248 for i in range(test_objects): |
| 249 self.CreateObject(bucket_uri=bucket_uri, |
| 250 object_name=self.MakeTempName('obj'), |
| 251 contents='test %d' % i) |
| 252 return bucket_uri |
| 253 |
| 254 def CreateVersionedBucket(self, bucket_name=None, test_objects=0): |
| 255 """Creates a versioned test bucket. |
| 256 |
| 257 The bucket and all of its contents will be deleted after the test. |
| 258 |
| 259 Args: |
| 260 bucket_name: Create the bucket with this name. If not provided, a |
| 261 temporary test bucket name is constructed. |
| 262 test_objects: The number of objects that should be placed in the bucket. |
| 263 Defaults to 0. |
| 264 |
| 265 Returns: |
| 266 StorageUri for the created bucket with versioning enabled. |
| 267 """ |
| 268 bucket_uri = self.CreateBucket(bucket_name=bucket_name, |
| 269 test_objects=test_objects) |
| 270 bucket_uri.configure_versioning(True) |
| 271 return bucket_uri |
| 272 |
| 273 def CreateObject(self, bucket_uri=None, object_name=None, contents=None, |
| 274 prefer_json_api=False): |
| 275 """Creates a test object. |
| 276 |
| 277 Args: |
| 278 bucket_uri: The URI of the bucket to place the object in. If not |
| 279 specified, a new temporary bucket is created. |
| 280 object_name: The name to use for the object. If not specified, a temporary |
| 281 test object name is constructed. |
| 282 contents: The contents to write to the object. If not specified, the key |
| 283 is not written to, which means that it isn't actually created |
| 284 yet on the server. |
| 285 prefer_json_api: If true, use the JSON creation functions where possible. |
| 286 |
| 287 Returns: |
| 288 A StorageUri for the created object. |
| 289 """ |
| 290 bucket_uri = bucket_uri or self.CreateBucket() |
| 291 |
| 292 if prefer_json_api and bucket_uri.scheme == 'gs' and contents: |
| 293 object_name = object_name or self.MakeTempName('obj') |
| 294 json_object = self.CreateObjectJson(contents=contents, |
| 295 bucket_name=bucket_uri.bucket_name, |
| 296 object_name=object_name) |
| 297 object_uri = bucket_uri.clone_replace_name(object_name) |
| 298 # pylint: disable=protected-access |
| 299 # Need to update the StorageUri with the correct values while |
| 300 # avoiding creating a versioned string. |
| 301 object_uri._update_from_values(None, |
| 302 json_object.generation, |
| 303 True, |
| 304 md5=(Base64ToHexHash(json_object.md5Hash), |
| 305 json_object.md5Hash.strip('\n"\''))) |
| 306 # pylint: enable=protected-access |
| 307 return object_uri |
| 308 |
| 309 bucket_uri = bucket_uri or self.CreateBucket() |
| 310 object_name = object_name or self.MakeTempName('obj') |
| 311 key_uri = bucket_uri.clone_replace_name(object_name) |
| 312 if contents is not None: |
| 313 key_uri.set_contents_from_string(contents) |
| 314 return key_uri |
| 315 |
| 316 def CreateBucketJson(self, bucket_name=None, test_objects=0, |
| 317 storage_class=None): |
| 318 """Creates a test bucket using the JSON API. |
| 319 |
| 320 The bucket and all of its contents will be deleted after the test. |
| 321 |
| 322 Args: |
| 323 bucket_name: Create the bucket with this name. If not provided, a |
| 324 temporary test bucket name is constructed. |
| 325 test_objects: The number of objects that should be placed in the bucket. |
| 326 Defaults to 0. |
| 327 storage_class: storage class to use. If not provided we us standard. |
| 328 |
| 329 Returns: |
| 330 Apitools Bucket for the created bucket. |
| 331 """ |
| 332 bucket_name = bucket_name or self.MakeTempName('bucket') |
| 333 bucket_metadata = None |
| 334 if storage_class: |
| 335 bucket_metadata = apitools_messages.Bucket( |
| 336 name=bucket_name.lower(), |
| 337 storageClass=storage_class) |
| 338 |
| 339 # TODO: Add retry and exponential backoff. |
| 340 bucket = self.json_api.CreateBucket(bucket_name.lower(), |
| 341 metadata=bucket_metadata) |
| 342 # Add bucket to list of buckets to be cleaned up. |
| 343 # TODO: Clean up JSON buckets using JSON API. |
| 344 self.bucket_uris.append( |
| 345 boto.storage_uri('gs://%s' % (bucket_name.lower()), |
| 346 suppress_consec_slashes=False)) |
| 347 for i in range(test_objects): |
| 348 self.CreateObjectJson(bucket_name=bucket_name, |
| 349 object_name=self.MakeTempName('obj'), |
| 350 contents='test %d' % i) |
| 351 return bucket |
| 352 |
| 353 def CreateObjectJson(self, contents, bucket_name=None, object_name=None): |
| 354 """Creates a test object (GCS provider only) using the JSON API. |
| 355 |
| 356 Args: |
| 357 contents: The contents to write to the object. |
| 358 bucket_name: Name of bucket to place the object in. If not |
| 359 specified, a new temporary bucket is created. |
| 360 object_name: The name to use for the object. If not specified, a temporary |
| 361 test object name is constructed. |
| 362 |
| 363 Returns: |
| 364 An apitools Object for the created object. |
| 365 """ |
| 366 bucket_name = bucket_name or self.CreateBucketJson().name |
| 367 object_name = object_name or self.MakeTempName('obj') |
| 368 object_metadata = apitools_messages.Object( |
| 369 name=object_name, |
| 370 bucket=bucket_name, |
| 371 contentType='application/octet-stream') |
| 372 return self.json_api.UploadObject(cStringIO.StringIO(contents), |
| 373 object_metadata, provider='gs') |
| 374 |
| 375 def RunGsUtil(self, cmd, return_status=False, return_stdout=False, |
| 376 return_stderr=False, expected_status=0, stdin=None): |
| 377 """Runs the gsutil command. |
| 378 |
| 379 Args: |
| 380 cmd: The command to run, as a list, e.g. ['cp', 'foo', 'bar'] |
| 381 return_status: If True, the exit status code is returned. |
| 382 return_stdout: If True, the standard output of the command is returned. |
| 383 return_stderr: If True, the standard error of the command is returned. |
| 384 expected_status: The expected return code. If not specified, defaults to |
| 385 0. If the return code is a different value, an exception |
| 386 is raised. |
| 387 stdin: A string of data to pipe to the process as standard input. |
| 388 |
| 389 Returns: |
| 390 A tuple containing the desired return values specified by the return_* |
| 391 arguments. |
| 392 """ |
| 393 cmd = ([gslib.GSUTIL_PATH] + ['--testexceptiontraces'] + |
| 394 ['-o', 'GSUtil:default_project_id=' + PopulateProjectId()] + |
| 395 cmd) |
| 396 if IS_WINDOWS: |
| 397 cmd = [sys.executable] + cmd |
| 398 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 399 stdin=subprocess.PIPE) |
| 400 (stdout, stderr) = p.communicate(stdin) |
| 401 status = p.returncode |
| 402 |
| 403 if expected_status is not None: |
| 404 self.assertEqual( |
| 405 status, expected_status, |
| 406 msg='Expected status %d, got %d.\nCommand:\n%s\n\nstderr:\n%s' % ( |
| 407 expected_status, status, ' '.join(cmd), stderr)) |
| 408 |
| 409 toreturn = [] |
| 410 if return_status: |
| 411 toreturn.append(status) |
| 412 if return_stdout: |
| 413 if IS_WINDOWS: |
| 414 stdout = stdout.replace('\r\n', '\n') |
| 415 toreturn.append(stdout) |
| 416 if return_stderr: |
| 417 if IS_WINDOWS: |
| 418 stderr = stderr.replace('\r\n', '\n') |
| 419 toreturn.append(stderr) |
| 420 |
| 421 if len(toreturn) == 1: |
| 422 return toreturn[0] |
| 423 elif toreturn: |
| 424 return tuple(toreturn) |
| 425 |
| 426 def RunGsUtilTabCompletion(self, cmd, expected_results=None): |
| 427 """Runs the gsutil command in tab completion mode. |
| 428 |
| 429 Args: |
| 430 cmd: The command to run, as a list, e.g. ['cp', 'foo', 'bar'] |
| 431 expected_results: The expected tab completion results for the given input. |
| 432 """ |
| 433 cmd = [gslib.GSUTIL_PATH] + ['--testexceptiontraces'] + cmd |
| 434 cmd_str = ' '.join(cmd) |
| 435 |
| 436 @Retry(AssertionError, tries=5, timeout_secs=1) |
| 437 def _RunTabCompletion(): |
| 438 """Runs the tab completion operation with retries.""" |
| 439 results_string = None |
| 440 with tempfile.NamedTemporaryFile( |
| 441 delete=False) as tab_complete_result_file: |
| 442 # argcomplete returns results via the '8' file descriptor so we |
| 443 # redirect to a file so we can capture them. |
| 444 cmd_str_with_result_redirect = '%s 8>%s' % ( |
| 445 cmd_str, tab_complete_result_file.name) |
| 446 env = os.environ.copy() |
| 447 env['_ARGCOMPLETE'] = '1' |
| 448 env['COMP_LINE'] = cmd_str |
| 449 env['COMP_POINT'] = str(len(cmd_str)) |
| 450 subprocess.call(cmd_str_with_result_redirect, env=env, shell=True) |
| 451 results_string = tab_complete_result_file.read().decode( |
| 452 locale.getpreferredencoding()) |
| 453 if results_string: |
| 454 results = results_string.split('\013') |
| 455 else: |
| 456 results = [] |
| 457 self.assertEqual(results, expected_results) |
| 458 |
| 459 # When tests are run in parallel, tab completion could take a long time, |
| 460 # so choose a long timeout value. |
| 461 with SetBotoConfigForTest([('GSUtil', 'tab_completion_timeout', '120')]): |
| 462 _RunTabCompletion() |
| 463 |
| 464 @contextmanager |
| 465 def SetAnonymousBotoCreds(self): |
| 466 boto_config_path = self.CreateTempFile( |
| 467 contents=BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING) |
| 468 with SetBotoConfigFileForTest(boto_config_path): |
| 469 # Make sure to reset Developer Shell credential port so that the child |
| 470 # gsutil process is really anonymous. |
| 471 with SetEnvironmentForTest({'DEVSHELL_CLIENT_PORT': None}): |
| 472 yield |
OLD | NEW |