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 |