Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(581)

Side by Side Diff: gslib/commands/perfdiag.py

Issue 698893003: Update checked in version of gsutil to version 4.6 (Closed) Base URL: http://dart.googlecode.com/svn/third_party/gsutil/
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « gslib/commands/notification.py ('k') | gslib/commands/rb.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # -*- coding: utf-8 -*-
1 # Copyright 2012 Google Inc. All Rights Reserved. 2 # Copyright 2012 Google Inc. All Rights Reserved.
2 # 3 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License. 5 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at 6 # You may obtain a copy of the License at
6 # 7 #
7 # http://www.apache.org/licenses/LICENSE-2.0 8 # http://www.apache.org/licenses/LICENSE-2.0
8 # 9 #
9 # Unless required by applicable law or agreed to in writing, software 10 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 13 # See the License for the specific language governing permissions and
13 # limitations under the License. 14 # limitations under the License.
14
15 """Contains the perfdiag gsutil command.""" 15 """Contains the perfdiag gsutil command."""
16 16
17 # Get the system logging module, not our local logging module.
18 from __future__ import absolute_import 17 from __future__ import absolute_import
19 18
20 import calendar 19 import calendar
21 from collections import defaultdict 20 from collections import defaultdict
22 import contextlib 21 import contextlib
22 import cStringIO
23 import datetime 23 import datetime
24 import httplib
24 import json 25 import json
25 import logging 26 import logging
26 import math 27 import math
27 import multiprocessing 28 import multiprocessing
28 import os 29 import os
29 import random 30 import random
30 import re 31 import re
31 import socket 32 import socket
32 import string 33 import string
33 import subprocess 34 import subprocess
34 import tempfile 35 import tempfile
35 import time 36 import time
36 37
38 from apiclient import errors as apiclient_errors
37 import boto 39 import boto
38 from boto.utils import compute_md5
39 import boto.gs.connection 40 import boto.gs.connection
40 41
41 import gslib 42 import gslib
43 from gslib.cloud_api import NotFoundException
44 from gslib.cloud_api import ServiceException
45 from gslib.cloud_api_helper import GetDownloadSerializationDict
42 from gslib.command import Command 46 from gslib.command import Command
43 from gslib.command import COMMAND_NAME
44 from gslib.command import COMMAND_NAME_ALIASES
45 from gslib.command import DummyArgChecker 47 from gslib.command import DummyArgChecker
46 from gslib.command import FILE_URIS_OK
47 from gslib.command import MAX_ARGS
48 from gslib.command import MIN_ARGS
49 from gslib.command import PROVIDER_URIS_OK
50 from gslib.command import SUPPORTED_SUB_ARGS
51 from gslib.command import URIS_START_ARG
52 #from gslib.command_runner import CommandRunner
53 from gslib.commands import config 48 from gslib.commands import config
49 from gslib.cs_api_map import ApiSelector
54 from gslib.exception import CommandException 50 from gslib.exception import CommandException
55 from gslib.help_provider import HELP_NAME 51 from gslib.hashing_helper import CalculateB64EncodedMd5FromContents
56 from gslib.help_provider import HELP_NAME_ALIASES 52 from gslib.storage_url import StorageUrlFromString
57 from gslib.help_provider import HELP_ONE_LINE_SUMMARY 53 from gslib.third_party.storage_apitools import storage_v1_messages as apitools_m essages
58 from gslib.help_provider import HELP_TEXT 54 from gslib.util import GetCloudApiInstance
59 from gslib.help_provider import HELP_TYPE 55 from gslib.util import GetMaxRetryDelay
60 from gslib.help_provider import HelpType
61 from gslib.util import GetBotoConfigFileList
62 from gslib.util import HumanReadableToBytes 56 from gslib.util import HumanReadableToBytes
63 from gslib.util import IS_LINUX 57 from gslib.util import IS_LINUX
64 from gslib.util import MakeBitsHumanReadable 58 from gslib.util import MakeBitsHumanReadable
65 from gslib.util import MakeHumanReadable 59 from gslib.util import MakeHumanReadable
66 from gslib.util import Percentile 60 from gslib.util import Percentile
61 from gslib.util import ResumableThreshold
67 62
68 _detailed_help_text = (""" 63 _DETAILED_HELP_TEXT = ("""
69 <B>SYNOPSIS</B> 64 <B>SYNOPSIS</B>
70 gsutil perfdiag [-i in.json] [-o out.json] [-n iterations] [-c processes] 65 gsutil perfdiag [-i in.json] [-o out.json] [-n iterations] [-c processes]
71 [-k threads] [-s size] [-t tests] uri... 66 [-k threads] [-s size] [-t tests] url...
72 67
73 68
74 <B>DESCRIPTION</B> 69 <B>DESCRIPTION</B>
75 The perfdiag command runs a suite of diagnostic tests for a given Google 70 The perfdiag command runs a suite of diagnostic tests for a given Google
76 Storage bucket. 71 Storage bucket.
77 72
78 The 'uri' parameter must name an existing bucket (e.g. gs://foo) to which 73 The 'url' parameter must name an existing bucket (e.g. gs://foo) to which
79 the user has write permission. Several test files will be uploaded to and 74 the user has write permission. Several test files will be uploaded to and
80 downloaded from this bucket. All test files will be deleted at the completion 75 downloaded from this bucket. All test files will be deleted at the completion
81 of the diagnostic if it finishes successfully. 76 of the diagnostic if it finishes successfully.
82 77
83 gsutil performance can be impacted by many factors at the client, server, 78 gsutil performance can be impacted by many factors at the client, server,
84 and in-between, such as: CPU speed; available memory; the access path to the 79 and in-between, such as: CPU speed; available memory; the access path to the
85 local disk; network bandwidth; contention and error rates along the path 80 local disk; network bandwidth; contention and error rates along the path
86 between gsutil and Google; operating system buffering configuration; and 81 between gsutil and Google; operating system buffering configuration; and
87 firewalls and other network elements. The perfdiag command is provided so 82 firewalls and other network elements. The perfdiag command is provided so
88 that customers can run a known measurement suite when troubleshooting 83 that customers can run a known measurement suite when troubleshooting
(...skipping 26 matching lines...) Expand all
115 110
116 -t Sets the list of diagnostic tests to perform. The default is to 111 -t Sets the list of diagnostic tests to perform. The default is to
117 run all diagnostic tests. Must be a comma-separated list 112 run all diagnostic tests. Must be a comma-separated list
118 containing one or more of the following: 113 containing one or more of the following:
119 114
120 lat 115 lat
121 Runs N iterations (set with -n) of writing the file, 116 Runs N iterations (set with -n) of writing the file,
122 retrieving its metadata, reading the file, and deleting 117 retrieving its metadata, reading the file, and deleting
123 the file. Records the latency of each operation. 118 the file. Records the latency of each operation.
124 119
120 list
121 Write N (set with -n) objects to the bucket, record how long
122 it takes for the eventually consistent listing call to return
123 the N objects in its result, delete the N objects, then record
124 how long it takes listing to stop returning the N objects.
125 This test is off by default.
126
125 rthru 127 rthru
126 Runs N (set with -n) read operations, with at most C 128 Runs N (set with -n) read operations, with at most C
127 (set with -c) reads outstanding at any given time. 129 (set with -c) reads outstanding at any given time.
128 130
129 wthru 131 wthru
130 Runs N (set with -n) write operations, with at most C 132 Runs N (set with -n) write operations, with at most C
131 (set with -c) writes outstanding at any given time. 133 (set with -c) writes outstanding at any given time.
132 134
133 -m Adds metadata to the result JSON file. Multiple -m values can be 135 -m Adds metadata to the result JSON file. Multiple -m values can be
134 specified. Example: 136 specified. Example:
(...skipping 21 matching lines...) Expand all
156 158
157 Note that HTTP responses are only recorded when the request was made in a 159 Note that HTTP responses are only recorded when the request was made in a
158 single process. When using multiple processes or threads, read and write 160 single process. When using multiple processes or threads, read and write
159 throughput measurements are performed in an external process, so the 161 throughput measurements are performed in an external process, so the
160 availability numbers reported won't include the throughput measurements. 162 availability numbers reported won't include the throughput measurements.
161 163
162 164
163 <B>NOTE</B> 165 <B>NOTE</B>
164 The perfdiag command collects system information. It collects your IP address, 166 The perfdiag command collects system information. It collects your IP address,
165 executes DNS queries to Google servers and collects the results, and collects 167 executes DNS queries to Google servers and collects the results, and collects
166 network statistics information from the output of netstat -s. None of this 168 network statistics information from the output of netstat -s. It will also
167 information will be sent to Google unless you choose to send it. 169 attempt to connect to your proxy server if you have one configured. None of
170 this information will be sent to Google unless you choose to send it.
168 """) 171 """)
169 172
170 def _DownloadKey(cls, key): 173
171 key.get_contents_to_file(cls.devnull, **cls.get_contents_to_file_args) 174 class Error(Exception):
172 175 """Base exception class for this module."""
173 def _UploadKey(cls, key): 176 pass
174 return key.set_contents_from_string(cls.file_contents[cls.thru_local_file], 177
175 md5=cls.file_md5s[cls.thru_local_file]) 178
176 179 class InvalidArgument(Error):
180 """Raised on invalid arguments to functions."""
181 pass
182
183
184 def _DownloadWrapper(cls, arg, thread_state=None):
185 cls.Download(arg, thread_state=thread_state)
186
187
188 def _UploadWrapper(cls, arg, thread_state=None):
189 cls.Upload(arg, thread_state=thread_state)
190
191
192 def _DeleteWrapper(cls, arg, thread_state=None):
193 cls.Delete(arg, thread_state=thread_state)
194
195
177 def _PerfdiagExceptionHandler(cls, e): 196 def _PerfdiagExceptionHandler(cls, e):
178 """Simple exception handler to allow post-completion status.""" 197 """Simple exception handler to allow post-completion status."""
179 cls.logger.error(str(e)) 198 cls.logger.error(str(e))
180 199
200
201 def _DummyTrackerCallback(_):
202 pass
203
181 204
182 class DummyFile(object): 205 class DummyFile(object):
183 """A dummy, file-like object that throws away everything written to it.""" 206 """A dummy, file-like object that throws away everything written to it."""
184 207
185 def write(self, *args, **kwargs): # pylint: disable-msg=C6409 208 def write(self, *args, **kwargs): # pylint: disable=invalid-name
186 pass 209 pass
187 210
188 211
189 class PerfDiagCommand(Command): 212 class PerfDiagCommand(Command):
190 """Implementation of gsutil perfdiag command.""" 213 """Implementation of gsutil perfdiag command."""
191 214
192 # Command specification (processed by parent class). 215 # Command specification. See base class for documentation.
193 command_spec = { 216 command_spec = Command.CreateCommandSpec(
194 # Name of command. 217 'perfdiag',
195 COMMAND_NAME: 'perfdiag', 218 command_name_aliases=['diag', 'diagnostic', 'perf', 'performance'],
196 # List of command name aliases. 219 min_args=0,
197 COMMAND_NAME_ALIASES: ['diag', 'diagnostic', 'perf', 'performance'], 220 max_args=1,
198 # Min number of args required by this command. 221 supported_sub_args='n:c:k:s:t:m:i:o:',
199 MIN_ARGS: 0, 222 file_url_ok=False,
200 # Max number of args required by this command, or NO_MAX. 223 provider_url_ok=False,
201 MAX_ARGS: 1, 224 urls_start_arg=0,
202 # Getopt-style string specifying acceptable sub args. 225 gs_api_support=[ApiSelector.XML, ApiSelector.JSON],
203 SUPPORTED_SUB_ARGS: 'n:c:k:s:t:m:i:o:', 226 gs_default_api=ApiSelector.JSON,
204 # True if file URIs acceptable for this command. 227 )
205 FILE_URIS_OK: False, 228 # Help specification. See help_provider.py for documentation.
206 # True if provider-only URIs acceptable for this command. 229 help_spec = Command.HelpSpec(
207 PROVIDER_URIS_OK: False, 230 help_name='perfdiag',
208 # Index in args of first URI arg. 231 help_name_aliases=[],
209 URIS_START_ARG: 0, 232 help_type='command_help',
210 } 233 help_one_line_summary='Run performance diagnostic',
211 help_spec = { 234 help_text=_DETAILED_HELP_TEXT,
212 # Name of command or auxiliary help info for which this help applies. 235 subcommand_help_text={},
213 HELP_NAME: 'perfdiag', 236 )
214 # List of help name aliases.
215 HELP_NAME_ALIASES: [],
216 # Type of help:
217 HELP_TYPE: HelpType.COMMAND_HELP,
218 # One line summary of this help.
219 HELP_ONE_LINE_SUMMARY: 'Run performance diagnostic',
220 # The full help text.
221 HELP_TEXT: _detailed_help_text,
222 }
223 237
224 # Byte sizes to use for testing files. 238 # Byte sizes to use for latency testing files.
225 # TODO: Consider letting the user specify these sizes with a configuration 239 # TODO: Consider letting the user specify these sizes with a configuration
226 # parameter. 240 # parameter.
227 test_file_sizes = ( 241 test_file_sizes = (
228 0, # 0 bytes 242 0, # 0 bytes
229 1024, # 1 KB 243 1024, # 1 KB
230 102400, # 100 KB 244 102400, # 100 KB
231 1048576, # 1MB 245 1048576, # 1MB
232 ) 246 )
233 247
248 # Test names.
249 RTHRU = 'rthru'
250 WTHRU = 'wthru'
251 LAT = 'lat'
252 LIST = 'list'
253
234 # List of all diagnostic tests. 254 # List of all diagnostic tests.
235 ALL_DIAG_TESTS = ('rthru', 'wthru', 'lat') 255 ALL_DIAG_TESTS = (RTHRU, WTHRU, LAT, LIST)
256 # List of diagnostic tests to run by default.
257 DEFAULT_DIAG_TESTS = (RTHRU, WTHRU, LAT)
236 258
237 # Google Cloud Storage API endpoint host. 259 # Google Cloud Storage XML API endpoint host.
238 GOOGLE_API_HOST = boto.gs.connection.GSConnection.DefaultHost 260 XML_API_HOST = boto.config.get(
261 'Credentials', 'gs_host', boto.gs.connection.GSConnection.DefaultHost)
262 # Google Cloud Storage XML API endpoint port.
263 XML_API_PORT = boto.config.get('Credentials', 'gs_port', 80)
239 264
240 # Maximum number of times to retry requests on 5xx errors. 265 # Maximum number of times to retry requests on 5xx errors.
241 MAX_SERVER_ERROR_RETRIES = 5 266 MAX_SERVER_ERROR_RETRIES = 5
242 # Maximum number of times to retry requests on more serious errors like 267 # Maximum number of times to retry requests on more serious errors like
243 # the socket breaking. 268 # the socket breaking.
244 MAX_TOTAL_RETRIES = 10 269 MAX_TOTAL_RETRIES = 10
245 270
246 # The default buffer size in boto's Key object is set to 8KB. This becomes a 271 # The default buffer size in boto's Key object is set to 8KB. This becomes a
247 # bottleneck at high throughput rates, so we increase it. 272 # bottleneck at high throughput rates, so we increase it.
248 KEY_BUFFER_SIZE = 16384 273 KEY_BUFFER_SIZE = 16384
249 274
250 # The maximum number of bytes to generate pseudo-randomly before beginning 275 # The maximum number of bytes to generate pseudo-randomly before beginning
251 # to repeat bytes. This number was chosen as the next prime larger than 5 MB. 276 # to repeat bytes. This number was chosen as the next prime larger than 5 MB.
252 MAX_UNIQUE_RANDOM_BYTES = 5242883 277 MAX_UNIQUE_RANDOM_BYTES = 5242883
253 278
279 # Maximum amount of time, in seconds, we will wait for object listings to
280 # reflect what we expect in the listing tests.
281 MAX_LISTING_WAIT_TIME = 60.0
282
254 def _Exec(self, cmd, raise_on_error=True, return_output=False, 283 def _Exec(self, cmd, raise_on_error=True, return_output=False,
255 mute_stderr=False): 284 mute_stderr=False):
256 """Executes a command in a subprocess. 285 """Executes a command in a subprocess.
257 286
258 Args: 287 Args:
259 cmd: List containing the command to execute. 288 cmd: List containing the command to execute.
260 raise_on_error: Whether or not to raise an exception when a process exits 289 raise_on_error: Whether or not to raise an exception when a process exits
261 with a non-zero return code. 290 with a non-zero return code.
262 return_output: If set to True, the return value of the function is the 291 return_output: If set to True, the return value of the function is the
263 stdout of the process. 292 stdout of the process.
264 mute_stderr: If set to True, the stderr of the process is not printed to 293 mute_stderr: If set to True, the stderr of the process is not printed to
265 the console. 294 the console.
266 295
267 Returns: 296 Returns:
268 The return code of the process or the stdout if return_output is set. 297 The return code of the process or the stdout if return_output is set.
269 298
270 Raises: 299 Raises:
271 Exception: If raise_on_error is set to True and any process exits with a 300 Exception: If raise_on_error is set to True and any process exits with a
272 non-zero return code. 301 non-zero return code.
273 """ 302 """
274 self.logger.debug('Running command: %s', cmd) 303 self.logger.debug('Running command: %s', cmd)
275 stderr = subprocess.PIPE if mute_stderr else None 304 stderr = subprocess.PIPE if mute_stderr else None
276 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr) 305 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr)
277 (stdoutdata, stderrdata) = p.communicate() 306 (stdoutdata, _) = p.communicate()
278 if raise_on_error and p.returncode: 307 if raise_on_error and p.returncode:
279 raise CommandException("Received non-zero return code (%d) from " 308 raise CommandException("Received non-zero return code (%d) from "
280 "subprocess '%s'." % (p.returncode, ' '.join(cmd))) 309 "subprocess '%s'." % (p.returncode, ' '.join(cmd)))
281 return stdoutdata if return_output else p.returncode 310 return stdoutdata if return_output else p.returncode
282 311
283 def _SetUp(self): 312 def _SetUp(self):
284 """Performs setup operations needed before diagnostics can be run.""" 313 """Performs setup operations needed before diagnostics can be run."""
285 314
286 # Stores test result data. 315 # Stores test result data.
287 self.results = {} 316 self.results = {}
(...skipping 14 matching lines...) Expand all
302 # Total number of socket errors. 331 # Total number of socket errors.
303 self.connection_breaks = 0 332 self.connection_breaks = 0
304 333
305 def _MakeFile(file_size): 334 def _MakeFile(file_size):
306 """Creates a temporary file of the given size and returns its path.""" 335 """Creates a temporary file of the given size and returns its path."""
307 fd, fpath = tempfile.mkstemp(suffix='.bin', prefix='gsutil_test_file', 336 fd, fpath = tempfile.mkstemp(suffix='.bin', prefix='gsutil_test_file',
308 text=False) 337 text=False)
309 self.file_sizes[fpath] = file_size 338 self.file_sizes[fpath] = file_size
310 random_bytes = os.urandom(min(file_size, self.MAX_UNIQUE_RANDOM_BYTES)) 339 random_bytes = os.urandom(min(file_size, self.MAX_UNIQUE_RANDOM_BYTES))
311 total_bytes = 0 340 total_bytes = 0
312 file_contents = "" 341 file_contents = ''
313 while total_bytes < file_size: 342 while total_bytes < file_size:
314 num_bytes = min(self.MAX_UNIQUE_RANDOM_BYTES, file_size - total_bytes) 343 num_bytes = min(self.MAX_UNIQUE_RANDOM_BYTES, file_size - total_bytes)
315 file_contents += random_bytes[:num_bytes] 344 file_contents += random_bytes[:num_bytes]
316 total_bytes += num_bytes 345 total_bytes += num_bytes
317 self.file_contents[fpath] = file_contents 346 self.file_contents[fpath] = file_contents
318 with os.fdopen(fd, 'wb') as f: 347 with os.fdopen(fd, 'wb') as f:
319 f.write(self.file_contents[fpath]) 348 f.write(self.file_contents[fpath])
320 with open(fpath, 'rb') as f: 349 with open(fpath, 'rb') as f:
321 self.file_md5s[fpath] = compute_md5(f) 350 self.file_md5s[fpath] = CalculateB64EncodedMd5FromContents(f)
322 return fpath 351 return fpath
323 352
324 # Create files for latency tests. 353 # Create files for latency tests.
325 for file_size in self.test_file_sizes: 354 for file_size in self.test_file_sizes:
326 fpath = _MakeFile(file_size) 355 fpath = _MakeFile(file_size)
327 self.latency_files.append(fpath) 356 self.latency_files.append(fpath)
328 357
329 # Creating a file for warming up the TCP connection. 358 # Creating a file for warming up the TCP connection.
330 self.tcp_warmup_file = _MakeFile(5 * 1024 * 1024) # 5 Megabytes. 359 self.tcp_warmup_file = _MakeFile(5 * 1024 * 1024) # 5 Megabytes.
331 # Remote file to use for TCP warmup. 360 # Remote file to use for TCP warmup.
332 self.tcp_warmup_remote_file = (str(self.bucket_uri) + 361 self.tcp_warmup_remote_file = (str(self.bucket_url) +
333 os.path.basename(self.tcp_warmup_file)) 362 os.path.basename(self.tcp_warmup_file))
334 363
335 # Local file on disk for write throughput tests. 364 # Local file on disk for write throughput tests.
336 self.thru_local_file = _MakeFile(self.thru_filesize) 365 self.thru_local_file = _MakeFile(self.thru_filesize)
337 # Remote file to write/read from during throughput tests. 366 # Remote file to write/read from during throughput tests.
338 self.thru_remote_file = (str(self.bucket_uri) + 367 self.thru_remote_file = (str(self.bucket_url) +
339 os.path.basename(self.thru_local_file)) 368 os.path.basename(self.thru_local_file))
340 # Dummy file buffer to use for downloading that goes nowhere. 369 # Dummy file buffer to use for downloading that goes nowhere.
341 self.devnull = DummyFile() 370 self.discard_sink = DummyFile()
342 371
343 def _TearDown(self): 372 def _TearDown(self):
344 """Performs operations to clean things up after performing diagnostics.""" 373 """Performs operations to clean things up after performing diagnostics."""
345 for fpath in self.latency_files + [self.thru_local_file, 374 for fpath in self.latency_files + [self.thru_local_file,
346 self.tcp_warmup_file]: 375 self.tcp_warmup_file]:
347 try: 376 try:
348 os.remove(fpath) 377 os.remove(fpath)
349 except OSError: 378 except OSError:
350 pass 379 pass
351 380
352 cleanup_files = [self.thru_local_file, self.tcp_warmup_file] 381 if self.LAT in self.diag_tests or self.WTHRU in self.diag_tests:
353 for f in cleanup_files: 382 cleanup_files = [self.thru_local_file, self.tcp_warmup_file]
383 for f in cleanup_files:
354 384
355 def _Delete(): 385 def _Delete():
356 k = self.bucket.key_class(self.bucket) 386 try:
357 k.name = os.path.basename(f) 387 self.gsutil_api.DeleteObject(self.bucket_url.bucket_name,
358 try: 388 os.path.basename(f),
359 k.delete() 389 provider=self.provider)
360 except boto.exception.BotoServerError as e: 390 except NotFoundException:
361 # Ignore not found errors since it's already gone. 391 pass
362 if e.status != 404:
363 raise
364 392
365 self._RunOperation(_Delete) 393 self._RunOperation(_Delete)
366 394
367 @contextlib.contextmanager 395 @contextlib.contextmanager
368 def _Time(self, key, bucket): 396 def _Time(self, key, bucket):
369 """A context manager that measures time. 397 """A context manager that measures time.
370 398
371 A context manager that prints a status message before and after executing 399 A context manager that prints a status message before and after executing
372 the inner command and times how long the inner command takes. Keeps track of 400 the inner command and times how long the inner command takes. Keeps track of
373 the timing, aggregated by the given key. 401 the timing, aggregated by the given key.
374 402
375 Args: 403 Args:
(...skipping 16 matching lines...) Expand all
392 Args: 420 Args:
393 func: The function to run. 421 func: The function to run.
394 422
395 Returns: 423 Returns:
396 True if the operation succeeds, False if aborted. 424 True if the operation succeeds, False if aborted.
397 """ 425 """
398 # We retry on httplib exceptions that can happen if the socket was closed 426 # We retry on httplib exceptions that can happen if the socket was closed
399 # by the remote party or the connection broke because of network issues. 427 # by the remote party or the connection broke because of network issues.
400 # Only the BotoServerError is counted as a 5xx error towards the retry 428 # Only the BotoServerError is counted as a 5xx error towards the retry
401 # limit. 429 # limit.
402 exceptions = list(self.bucket.connection.http_exceptions)
403 exceptions.append(boto.exception.BotoServerError)
404
405 success = False 430 success = False
406 server_error_retried = 0 431 server_error_retried = 0
407 total_retried = 0 432 total_retried = 0
408 i = 0 433 i = 0
434 return_val = None
409 while not success: 435 while not success:
410 next_sleep = random.random() * (2 ** i) + 1 436 next_sleep = min(random.random() * (2 ** i) + 1, GetMaxRetryDelay())
411 try: 437 try:
412 func() 438 return_val = func()
413 self.total_requests += 1 439 self.total_requests += 1
414 success = True 440 success = True
415 except tuple(exceptions) as e: 441 except tuple(self.exceptions) as e:
416 total_retried += 1 442 total_retried += 1
417 if total_retried > self.MAX_TOTAL_RETRIES: 443 if total_retried > self.MAX_TOTAL_RETRIES:
418 self.logger.info('Reached maximum total retries. Not retrying.') 444 self.logger.info('Reached maximum total retries. Not retrying.')
419 break 445 break
420 if isinstance(e, boto.exception.BotoServerError): 446 if (isinstance(e, apiclient_errors.HttpError) or
421 if e.status >= 500: 447 isinstance(e, ServiceException)):
422 self.error_responses_by_code[e.status] += 1 448 if isinstance(e, apiclient_errors.HttpError):
449 status = e.resp.status
450 else:
451 status = e.status
452 if status >= 500:
453 self.error_responses_by_code[status] += 1
423 self.total_requests += 1 454 self.total_requests += 1
424 self.request_errors += 1 455 self.request_errors += 1
425 server_error_retried += 1 456 server_error_retried += 1
426 time.sleep(next_sleep) 457 time.sleep(next_sleep)
427 else: 458 else:
428 raise 459 raise
429 if server_error_retried > self.MAX_SERVER_ERROR_RETRIES: 460 if server_error_retried > self.MAX_SERVER_ERROR_RETRIES:
430 self.logger.info( 461 self.logger.info(
431 'Reached maximum server error retries. Not retrying.') 462 'Reached maximum server error retries. Not retrying.')
432 break 463 break
433 else: 464 else:
434 self.connection_breaks += 1 465 self.connection_breaks += 1
435 return success 466 return return_val
436 467
437 def _RunLatencyTests(self): 468 def _RunLatencyTests(self):
438 """Runs latency tests.""" 469 """Runs latency tests."""
439 # Stores timing information for each category of operation. 470 # Stores timing information for each category of operation.
440 self.results['latency'] = defaultdict(list) 471 self.results['latency'] = defaultdict(list)
441 472
442 for i in range(self.num_iterations): 473 for i in range(self.num_iterations):
443 self.logger.info('\nRunning latency iteration %d...', i+1) 474 self.logger.info('\nRunning latency iteration %d...', i+1)
444 for fpath in self.latency_files: 475 for fpath in self.latency_files:
445 basename = os.path.basename(fpath) 476 basename = os.path.basename(fpath)
446 gsbucket = str(self.bucket_uri) 477 url = self.bucket_url.Clone()
447 gsuri = gsbucket + basename 478 url.object_name = basename
448 file_size = self.file_sizes[fpath] 479 file_size = self.file_sizes[fpath]
449 readable_file_size = MakeHumanReadable(file_size) 480 readable_file_size = MakeHumanReadable(file_size)
450 481
451 self.logger.info( 482 self.logger.info(
452 "\nFile of size %(size)s located on disk at '%(fpath)s' being " 483 "\nFile of size %s located on disk at '%s' being diagnosed in the "
453 "diagnosed in the cloud at '%(gsuri)s'." 484 "cloud at '%s'.", readable_file_size, fpath, url)
454 % {'size': readable_file_size, 'fpath': fpath, 'gsuri': gsuri})
455 485
456 k = self.bucket.key_class(self.bucket) 486 upload_target = StorageUrlToUploadObjectMetadata(url)
457 k.BufferSize = self.KEY_BUFFER_SIZE
458 k.key = basename
459 487
460 def _Upload(): 488 def _Upload():
489 io_fp = cStringIO.StringIO(self.file_contents[fpath])
461 with self._Time('UPLOAD_%d' % file_size, self.results['latency']): 490 with self._Time('UPLOAD_%d' % file_size, self.results['latency']):
462 k.set_contents_from_string(self.file_contents[fpath], 491 self.gsutil_api.UploadObject(
463 md5=self.file_md5s[fpath]) 492 io_fp, upload_target, size=file_size, provider=self.provider,
493 fields=['name'])
464 self._RunOperation(_Upload) 494 self._RunOperation(_Upload)
465 495
466 def _Metadata(): 496 def _Metadata():
467 with self._Time('METADATA_%d' % file_size, self.results['latency']): 497 with self._Time('METADATA_%d' % file_size, self.results['latency']):
468 k.exists() 498 return self.gsutil_api.GetObjectMetadata(
469 self._RunOperation(_Metadata) 499 url.bucket_name, url.object_name,
500 provider=self.provider, fields=['name', 'contentType',
501 'mediaLink', 'size'])
502 # Download will get the metadata first if we don't pass it in.
503 download_metadata = self._RunOperation(_Metadata)
504 serialization_dict = GetDownloadSerializationDict(download_metadata)
505 serialization_data = json.dumps(serialization_dict)
470 506
471 def _Download(): 507 def _Download():
472 with self._Time('DOWNLOAD_%d' % file_size, self.results['latency']): 508 with self._Time('DOWNLOAD_%d' % file_size, self.results['latency']):
473 k.get_contents_to_file(self.devnull, 509 self.gsutil_api.GetObjectMedia(
474 **self.get_contents_to_file_args) 510 url.bucket_name, url.object_name, self.discard_sink,
511 provider=self.provider, serialization_data=serialization_data)
475 self._RunOperation(_Download) 512 self._RunOperation(_Download)
476 513
477 def _Delete(): 514 def _Delete():
478 with self._Time('DELETE_%d' % file_size, self.results['latency']): 515 with self._Time('DELETE_%d' % file_size, self.results['latency']):
479 k.delete() 516 self.gsutil_api.DeleteObject(url.bucket_name, url.object_name,
517 provider=self.provider)
480 self._RunOperation(_Delete) 518 self._RunOperation(_Delete)
481 519
520 class _CpFilter(logging.Filter):
482 521
483 class _CpFilter(logging.Filter):
484 def filter(self, record): 522 def filter(self, record):
485 # Used to prevent cp._LogCopyOperation from spewing output from 523 # Used to prevent cp._LogCopyOperation from spewing output from
486 # subprocesses about every iteration. 524 # subprocesses about every iteration.
487 msg = record.getMessage() 525 msg = record.getMessage()
488 return not (('Copying file:///' in msg) or ('Copying gs://' in msg)) 526 return not (('Copying file:///' in msg) or ('Copying gs://' in msg) or
527 ('Computing CRC' in msg))
528
529 def _PerfdiagExceptionHandler(self, e):
530 """Simple exception handler to allow post-completion status."""
531 self.logger.error(str(e))
489 532
490 def _RunReadThruTests(self): 533 def _RunReadThruTests(self):
491 """Runs read throughput tests.""" 534 """Runs read throughput tests."""
492 self.results['read_throughput'] = {'file_size': self.thru_filesize, 535 self.results['read_throughput'] = {'file_size': self.thru_filesize,
493 'num_times': self.num_iterations, 536 'num_times': self.num_iterations,
494 'processes': self.processes, 537 'processes': self.processes,
495 'threads': self.threads} 538 'threads': self.threads}
496 539
497 # Copy the TCP warmup file. 540 # Copy the TCP warmup file.
498 warmup_key = self.bucket.key_class(self.bucket) 541 warmup_url = self.bucket_url.Clone()
499 warmup_key.key = os.path.basename(self.tcp_warmup_file) 542 warmup_url.object_name = os.path.basename(self.tcp_warmup_file)
543 warmup_target = StorageUrlToUploadObjectMetadata(warmup_url)
500 544
501 def _Upload1(): 545 def _Upload1():
502 warmup_key.set_contents_from_string( 546 self.gsutil_api.UploadObject(
503 self.file_contents[self.tcp_warmup_file], 547 cStringIO.StringIO(self.file_contents[self.tcp_warmup_file]),
504 md5=self.file_md5s[self.tcp_warmup_file]) 548 warmup_target, provider=self.provider, fields=['name'])
505 self._RunOperation(_Upload1) 549 self._RunOperation(_Upload1)
506 550
507 # Copy the file to remote location before reading. 551 # Copy the file to remote location before reading.
508 k = self.bucket.key_class(self.bucket) 552 thru_url = self.bucket_url.Clone()
509 k.BufferSize = self.KEY_BUFFER_SIZE 553 thru_url.object_name = os.path.basename(self.thru_local_file)
510 k.key = os.path.basename(self.thru_local_file) 554 thru_target = StorageUrlToUploadObjectMetadata(thru_url)
555 thru_target.md5Hash = self.file_md5s[self.thru_local_file]
511 556
557 # Get the mediaLink here so that we can pass it to download.
512 def _Upload2(): 558 def _Upload2():
513 k.set_contents_from_string(self.file_contents[self.thru_local_file], 559 return self.gsutil_api.UploadObject(
514 md5=self.file_md5s[self.thru_local_file]) 560 cStringIO.StringIO(self.file_contents[self.thru_local_file]),
515 self._RunOperation(_Upload2) 561 thru_target, provider=self.provider, size=self.thru_filesize,
562 fields=['name', 'mediaLink', 'size'])
563
564 # Get the metadata for the object so that we are just measuring performance
565 # on the actual bytes transfer.
566 download_metadata = self._RunOperation(_Upload2)
567 serialization_dict = GetDownloadSerializationDict(download_metadata)
568 serialization_data = json.dumps(serialization_dict)
516 569
517 if self.processes == 1 and self.threads == 1: 570 if self.processes == 1 and self.threads == 1:
518 571
519 # Warm up the TCP connection. 572 # Warm up the TCP connection.
520 def _Warmup(): 573 def _Warmup():
521 warmup_key.get_contents_to_file(self.devnull, 574 self.gsutil_api.GetObjectMedia(warmup_url.bucket_name,
522 **self.get_contents_to_file_args) 575 warmup_url.object_name,
576 self.discard_sink,
577 provider=self.provider)
523 self._RunOperation(_Warmup) 578 self._RunOperation(_Warmup)
524 579
525 times = [] 580 times = []
526 581
527 def _Download(): 582 def _Download():
528 t0 = time.time() 583 t0 = time.time()
529 k.get_contents_to_file(self.devnull, **self.get_contents_to_file_args) 584 self.gsutil_api.GetObjectMedia(
585 thru_url.bucket_name, thru_url.object_name, self.discard_sink,
586 provider=self.provider, serialization_data=serialization_data)
530 t1 = time.time() 587 t1 = time.time()
531 times.append(t1 - t0) 588 times.append(t1 - t0)
532 for _ in range(self.num_iterations): 589 for _ in range(self.num_iterations):
533 self._RunOperation(_Download) 590 self._RunOperation(_Download)
534 time_took = sum(times) 591 time_took = sum(times)
535 else: 592 else:
536 args = [k] * self.num_iterations 593 args = ([(thru_url.bucket_name, thru_url.object_name, serialization_data)]
594 * self.num_iterations)
537 self.logger.addFilter(self._CpFilter()) 595 self.logger.addFilter(self._CpFilter())
538 596
539 t0 = time.time() 597 t0 = time.time()
540 self.Apply(_DownloadKey, 598 self.Apply(_DownloadWrapper,
541 args, 599 args,
542 _PerfdiagExceptionHandler, 600 _PerfdiagExceptionHandler,
543 arg_checker=DummyArgChecker, 601 arg_checker=DummyArgChecker,
544 parallel_operations_override=True, 602 parallel_operations_override=True,
545 process_count=self.processes, 603 process_count=self.processes,
546 thread_count=self.threads) 604 thread_count=self.threads)
547 t1 = time.time() 605 t1 = time.time()
548 time_took = t1 - t0 606 time_took = t1 - t0
549 607
550 total_bytes_copied = self.thru_filesize * self.num_iterations 608 total_bytes_copied = self.thru_filesize * self.num_iterations
551 bytes_per_second = total_bytes_copied / time_took 609 bytes_per_second = total_bytes_copied / time_took
552 610
553 self.results['read_throughput']['time_took'] = time_took 611 self.results['read_throughput']['time_took'] = time_took
554 self.results['read_throughput']['total_bytes_copied'] = total_bytes_copied 612 self.results['read_throughput']['total_bytes_copied'] = total_bytes_copied
555 self.results['read_throughput']['bytes_per_second'] = bytes_per_second 613 self.results['read_throughput']['bytes_per_second'] = bytes_per_second
556 614
557 def _RunWriteThruTests(self): 615 def _RunWriteThruTests(self):
558 """Runs write throughput tests.""" 616 """Runs write throughput tests."""
559 self.results['write_throughput'] = {'file_size': self.thru_filesize, 617 self.results['write_throughput'] = {'file_size': self.thru_filesize,
560 'num_copies': self.num_iterations, 618 'num_copies': self.num_iterations,
561 'processes': self.processes, 619 'processes': self.processes,
562 'threads': self.threads} 620 'threads': self.threads}
563 621
564 k = self.bucket.key_class(self.bucket) 622 warmup_url = self.bucket_url.Clone()
565 k.BufferSize = self.KEY_BUFFER_SIZE 623 warmup_url.object_name = os.path.basename(self.tcp_warmup_file)
566 k.key = os.path.basename(self.thru_local_file) 624 warmup_target = StorageUrlToUploadObjectMetadata(warmup_url)
625
626 thru_url = self.bucket_url.Clone()
627 thru_url.object_name = os.path.basename(self.thru_local_file)
628 thru_target = StorageUrlToUploadObjectMetadata(thru_url)
629 thru_tuples = []
630 for i in xrange(self.num_iterations):
631 # Create a unique name for each uploaded object. Otherwise,
632 # the XML API would fail when trying to non-atomically get metadata
633 # for the object that gets blown away by the overwrite.
634 thru_tuples.append(UploadObjectTuple(
635 thru_target.bucket, thru_target.name + str(i),
636 filepath=self.thru_local_file))
637
567 if self.processes == 1 and self.threads == 1: 638 if self.processes == 1 and self.threads == 1:
568 # Warm up the TCP connection. 639 # Warm up the TCP connection.
569 warmup_key = self.bucket.key_class(self.bucket)
570 warmup_key.key = os.path.basename(self.tcp_warmup_file)
571
572 def _Warmup(): 640 def _Warmup():
573 warmup_key.set_contents_from_string( 641 self.gsutil_api.UploadObject(
574 self.file_contents[self.tcp_warmup_file], 642 cStringIO.StringIO(self.file_contents[self.tcp_warmup_file]),
575 md5=self.file_md5s[self.tcp_warmup_file]) 643 warmup_target, provider=self.provider, size=self.thru_filesize,
644 fields=['name'])
576 self._RunOperation(_Warmup) 645 self._RunOperation(_Warmup)
577 646
578 times = [] 647 times = []
579 648
580 def _Upload(): 649 for i in xrange(self.num_iterations):
581 t0 = time.time() 650 thru_tuple = thru_tuples[i]
582 k.set_contents_from_string(self.file_contents[self.thru_local_file], 651 def _Upload():
583 md5=self.file_md5s[self.thru_local_file]) 652 """Uploads the write throughput measurement object."""
584 t1 = time.time() 653 upload_target = apitools_messages.Object(
585 times.append(t1 - t0) 654 bucket=thru_tuple.bucket_name, name=thru_tuple.object_name,
586 for _ in range(self.num_iterations): 655 md5Hash=thru_tuple.md5)
656 io_fp = cStringIO.StringIO(self.file_contents[self.thru_local_file])
657 t0 = time.time()
658 if self.thru_filesize < ResumableThreshold():
659 self.gsutil_api.UploadObject(
660 io_fp, upload_target, provider=self.provider,
661 size=self.thru_filesize, fields=['name'])
662 else:
663 self.gsutil_api.UploadObjectResumable(
664 io_fp, upload_target, provider=self.provider,
665 size=self.thru_filesize, fields=['name'],
666 tracker_callback=_DummyTrackerCallback)
667
668 t1 = time.time()
669 times.append(t1 - t0)
670
587 self._RunOperation(_Upload) 671 self._RunOperation(_Upload)
588 time_took = sum(times) 672 time_took = sum(times)
589 673
590 else: 674 else:
591 args = [k] * self.num_iterations 675 args = thru_tuples
592 t0 = time.time() 676 t0 = time.time()
593 self.Apply(_UploadKey, 677 self.Apply(_UploadWrapper,
594 args, 678 args,
595 _PerfdiagExceptionHandler, 679 _PerfdiagExceptionHandler,
596 arg_checker=DummyArgChecker, 680 arg_checker=DummyArgChecker,
597 parallel_operations_override=True, 681 parallel_operations_override=True,
598 process_count=self.processes, 682 process_count=self.processes,
599 thread_count=self.threads) 683 thread_count=self.threads)
600 t1 = time.time() 684 t1 = time.time()
601 time_took = t1 - t0 685 time_took = t1 - t0
602 686
603 total_bytes_copied = self.thru_filesize * self.num_iterations 687 total_bytes_copied = self.thru_filesize * self.num_iterations
604 bytes_per_second = total_bytes_copied / time_took 688 bytes_per_second = total_bytes_copied / time_took
605 689
606 self.results['write_throughput']['time_took'] = time_took 690 self.results['write_throughput']['time_took'] = time_took
607 self.results['write_throughput']['total_bytes_copied'] = total_bytes_copied 691 self.results['write_throughput']['total_bytes_copied'] = total_bytes_copied
608 self.results['write_throughput']['bytes_per_second'] = bytes_per_second 692 self.results['write_throughput']['bytes_per_second'] = bytes_per_second
609 693
694 def _RunListTests(self):
695 """Runs eventual consistency listing latency tests."""
696 self.results['listing'] = {'num_files': self.num_iterations}
697
698 # Generate N random object names to put in the bucket.
699 list_prefix = 'gsutil-perfdiag-list-'
700 list_objects = []
701 for _ in xrange(self.num_iterations):
702 list_objects.append(
703 u'%s%s' % (list_prefix, os.urandom(20).encode('hex')))
704
705 # Add the objects to the bucket.
706 self.logger.info(
707 '\nWriting %s objects for listing test...', self.num_iterations)
708 empty_md5 = CalculateB64EncodedMd5FromContents(cStringIO.StringIO(''))
709 args = [
710 UploadObjectTuple(self.bucket_url.bucket_name, name, md5=empty_md5,
711 contents='') for name in list_objects]
712 self.Apply(_UploadWrapper, args, _PerfdiagExceptionHandler,
713 arg_checker=DummyArgChecker)
714
715 list_latencies = []
716 files_seen = []
717 total_start_time = time.time()
718 expected_objects = set(list_objects)
719 found_objects = set()
720
721 def _List():
722 """Lists and returns objects in the bucket. Also records latency."""
723 t0 = time.time()
724 objects = list(self.gsutil_api.ListObjects(
725 self.bucket_url.bucket_name, prefix=list_prefix, delimiter='/',
726 provider=self.provider, fields=['items/name']))
727 t1 = time.time()
728 list_latencies.append(t1 - t0)
729 return set([obj.data.name for obj in objects])
730
731 self.logger.info(
732 'Listing bucket %s waiting for %s objects to appear...',
733 self.bucket_url.bucket_name, self.num_iterations)
734 while expected_objects - found_objects:
735 def _ListAfterUpload():
736 names = _List()
737 found_objects.update(names & expected_objects)
738 files_seen.append(len(found_objects))
739 self._RunOperation(_ListAfterUpload)
740 if expected_objects - found_objects:
741 if time.time() - total_start_time > self.MAX_LISTING_WAIT_TIME:
742 self.logger.warning('Maximum time reached waiting for listing.')
743 break
744 total_end_time = time.time()
745
746 self.results['listing']['insert'] = {
747 'num_listing_calls': len(list_latencies),
748 'list_latencies': list_latencies,
749 'files_seen_after_listing': files_seen,
750 'time_took': total_end_time - total_start_time,
751 }
752
753 self.logger.info(
754 'Deleting %s objects for listing test...', self.num_iterations)
755 self.Apply(_DeleteWrapper, args, _PerfdiagExceptionHandler,
756 arg_checker=DummyArgChecker)
757
758 self.logger.info(
759 'Listing bucket %s waiting for %s objects to disappear...',
760 self.bucket_url.bucket_name, self.num_iterations)
761 list_latencies = []
762 files_seen = []
763 total_start_time = time.time()
764 found_objects = set(list_objects)
765 while found_objects:
766 def _ListAfterDelete():
767 names = _List()
768 found_objects.intersection_update(names)
769 files_seen.append(len(found_objects))
770 self._RunOperation(_ListAfterDelete)
771 if found_objects:
772 if time.time() - total_start_time > self.MAX_LISTING_WAIT_TIME:
773 self.logger.warning('Maximum time reached waiting for listing.')
774 break
775 total_end_time = time.time()
776
777 self.results['listing']['delete'] = {
778 'num_listing_calls': len(list_latencies),
779 'list_latencies': list_latencies,
780 'files_seen_after_listing': files_seen,
781 'time_took': total_end_time - total_start_time,
782 }
783
784 def Upload(self, thru_tuple, thread_state=None):
785 gsutil_api = GetCloudApiInstance(self, thread_state)
786
787 md5hash = thru_tuple.md5
788 contents = thru_tuple.contents
789 if thru_tuple.filepath:
790 md5hash = self.file_md5s[thru_tuple.filepath]
791 contents = self.file_contents[thru_tuple.filepath]
792
793 upload_target = apitools_messages.Object(
794 bucket=thru_tuple.bucket_name, name=thru_tuple.object_name,
795 md5Hash=md5hash)
796 file_size = len(contents)
797 if file_size < ResumableThreshold():
798 gsutil_api.UploadObject(
799 cStringIO.StringIO(contents), upload_target,
800 provider=self.provider, size=file_size, fields=['name'])
801 else:
802 gsutil_api.UploadObjectResumable(
803 cStringIO.StringIO(contents), upload_target,
804 provider=self.provider, size=file_size, fields=['name'],
805 tracker_callback=_DummyTrackerCallback)
806
807 def Download(self, download_tuple, thread_state=None):
808 """Downloads a file.
809
810 Args:
811 download_tuple: (bucket name, object name, serialization data for object).
812 thread_state: gsutil Cloud API instance to use for the download.
813 """
814 gsutil_api = GetCloudApiInstance(self, thread_state)
815 gsutil_api.GetObjectMedia(
816 download_tuple[0], download_tuple[1], self.discard_sink,
817 provider=self.provider, serialization_data=download_tuple[2])
818
819 def Delete(self, thru_tuple, thread_state=None):
820 gsutil_api = thread_state or self.gsutil_api
821 gsutil_api.DeleteObject(
822 thru_tuple.bucket_name, thru_tuple.object_name, provider=self.provider)
823
610 def _GetDiskCounters(self): 824 def _GetDiskCounters(self):
611 """Retrieves disk I/O statistics for all disks. 825 """Retrieves disk I/O statistics for all disks.
612 826
613 Adapted from the psutil module's psutil._pslinux.disk_io_counters: 827 Adapted from the psutil module's psutil._pslinux.disk_io_counters:
614 http://code.google.com/p/psutil/source/browse/trunk/psutil/_pslinux.py 828 http://code.google.com/p/psutil/source/browse/trunk/psutil/_pslinux.py
615 829
616 Originally distributed under under a BSD license. 830 Originally distributed under under a BSD license.
617 Original Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola. 831 Original Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola.
618 832
619 Returns: 833 Returns:
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after
695 result['tcp_retransmit'] = None 909 result['tcp_retransmit'] = None
696 result['tcp_received'] = None 910 result['tcp_received'] = None
697 result['tcp_sent'] = None 911 result['tcp_sent'] = None
698 912
699 return result 913 return result
700 914
701 def _CollectSysInfo(self): 915 def _CollectSysInfo(self):
702 """Collects system information.""" 916 """Collects system information."""
703 sysinfo = {} 917 sysinfo = {}
704 918
705 # All exceptions that might be thrown from socket module calls. 919 # All exceptions that might be raised from socket module calls.
706 socket_errors = ( 920 socket_errors = (
707 socket.error, socket.herror, socket.gaierror, socket.timeout) 921 socket.error, socket.herror, socket.gaierror, socket.timeout)
708 922
709 # Find out whether HTTPS is enabled in Boto. 923 # Find out whether HTTPS is enabled in Boto.
710 sysinfo['boto_https_enabled'] = boto.config.get('Boto', 'is_secure', True) 924 sysinfo['boto_https_enabled'] = boto.config.get('Boto', 'is_secure', True)
925
926 # Look up proxy info.
927 proxy_host = boto.config.get('Boto', 'proxy', None)
928 proxy_port = boto.config.getint('Boto', 'proxy_port', 0)
929 sysinfo['using_proxy'] = bool(proxy_host)
930
931 if boto.config.get('Boto', 'proxy_rdns', False):
932 self.logger.info('DNS lookups are disallowed in this environment, so '
933 'some information is not included in this perfdiag run.')
934
711 # Get the local IP address from socket lib. 935 # Get the local IP address from socket lib.
712 try: 936 try:
713 sysinfo['ip_address'] = socket.gethostbyname(socket.gethostname()) 937 sysinfo['ip_address'] = socket.gethostbyname(socket.gethostname())
714 except socket_errors: 938 except socket_errors:
715 sysinfo['ip_address'] = '' 939 sysinfo['ip_address'] = ''
716 # Record the temporary directory used since it can affect performance, e.g. 940 # Record the temporary directory used since it can affect performance, e.g.
717 # when on a networked filesystem. 941 # when on a networked filesystem.
718 sysinfo['tempdir'] = tempfile.gettempdir() 942 sysinfo['tempdir'] = tempfile.gettempdir()
719 943
720 # Produces an RFC 2822 compliant GMT timestamp. 944 # Produces an RFC 2822 compliant GMT timestamp.
721 sysinfo['gmt_timestamp'] = time.strftime('%a, %d %b %Y %H:%M:%S +0000', 945 sysinfo['gmt_timestamp'] = time.strftime('%a, %d %b %Y %H:%M:%S +0000',
722 time.gmtime()) 946 time.gmtime())
723 947
724 # Execute a CNAME lookup on Google DNS to find what Google server 948 # Execute a CNAME lookup on Google DNS to find what Google server
725 # it's routing to. 949 # it's routing to.
726 cmd = ['nslookup', '-type=CNAME', self.GOOGLE_API_HOST] 950 cmd = ['nslookup', '-type=CNAME', self.XML_API_HOST]
727 try: 951 try:
728 nslookup_cname_output = self._Exec(cmd, return_output=True) 952 nslookup_cname_output = self._Exec(cmd, return_output=True)
729 m = re.search(r' = (?P<googserv>[^.]+)\.', nslookup_cname_output) 953 m = re.search(r' = (?P<googserv>[^.]+)\.', nslookup_cname_output)
730 sysinfo['googserv_route'] = m.group('googserv') if m else None 954 sysinfo['googserv_route'] = m.group('googserv') if m else None
731 except OSError: 955 except (CommandException, OSError):
732 sysinfo['googserv_route'] = '' 956 sysinfo['googserv_route'] = ''
733 957
958 # Try to determine the latency of a DNS lookup for the Google hostname
959 # endpoint. Note: we don't piggyback on gethostbyname_ex below because
960 # the _ex version requires an extra RTT.
961 try:
962 t0 = time.time()
963 socket.gethostbyname(self.XML_API_HOST)
964 t1 = time.time()
965 sysinfo['google_host_dns_latency'] = t1 - t0
966 except socket_errors:
967 pass
968
734 # Look up IP addresses for Google Server. 969 # Look up IP addresses for Google Server.
735 try: 970 try:
736 (hostname, aliaslist, ipaddrlist) = socket.gethostbyname_ex( 971 (hostname, _, ipaddrlist) = socket.gethostbyname_ex(self.XML_API_HOST)
737 self.GOOGLE_API_HOST)
738 sysinfo['googserv_ips'] = ipaddrlist 972 sysinfo['googserv_ips'] = ipaddrlist
739 except socket_errors: 973 except socket_errors:
974 ipaddrlist = []
740 sysinfo['googserv_ips'] = [] 975 sysinfo['googserv_ips'] = []
741 976
742 # Reverse lookup the hostnames for the Google Server IPs. 977 # Reverse lookup the hostnames for the Google Server IPs.
743 sysinfo['googserv_hostnames'] = [] 978 sysinfo['googserv_hostnames'] = []
744 for googserv_ip in ipaddrlist: 979 for googserv_ip in ipaddrlist:
745 try: 980 try:
746 (hostname, aliaslist, ipaddrlist) = socket.gethostbyaddr(googserv_ip) 981 (hostname, _, ipaddrlist) = socket.gethostbyaddr(googserv_ip)
747 sysinfo['googserv_hostnames'].append(hostname) 982 sysinfo['googserv_hostnames'].append(hostname)
748 except socket_errors: 983 except socket_errors:
749 pass 984 pass
750 985
751 # Query o-o to find out what the Google DNS thinks is the user's IP. 986 # Query o-o to find out what the Google DNS thinks is the user's IP.
752 try: 987 try:
753 cmd = ['nslookup', '-type=TXT', 'o-o.myaddr.google.com.'] 988 cmd = ['nslookup', '-type=TXT', 'o-o.myaddr.google.com.']
754 nslookup_txt_output = self._Exec(cmd, return_output=True) 989 nslookup_txt_output = self._Exec(cmd, return_output=True)
755 m = re.search(r'text\s+=\s+"(?P<dnsip>[\.\d]+)"', nslookup_txt_output) 990 m = re.search(r'text\s+=\s+"(?P<dnsip>[\.\d]+)"', nslookup_txt_output)
756 sysinfo['dns_o-o_ip'] = m.group('dnsip') if m else None 991 sysinfo['dns_o-o_ip'] = m.group('dnsip') if m else None
757 except OSError: 992 except (CommandException, OSError):
758 sysinfo['dns_o-o_ip'] = '' 993 sysinfo['dns_o-o_ip'] = ''
759 994
995 # Try to determine the latency of connecting to the Google hostname
996 # endpoint.
997 sysinfo['google_host_connect_latencies'] = {}
998 for googserv_ip in ipaddrlist:
999 try:
1000 sock = socket.socket()
1001 t0 = time.time()
1002 sock.connect((googserv_ip, self.XML_API_PORT))
1003 t1 = time.time()
1004 sysinfo['google_host_connect_latencies'][googserv_ip] = t1 - t0
1005 except socket_errors:
1006 pass
1007
1008 # If using a proxy, try to determine the latency of a DNS lookup to resolve
1009 # the proxy hostname and the latency of connecting to the proxy.
1010 if proxy_host:
1011 proxy_ip = None
1012 try:
1013 t0 = time.time()
1014 proxy_ip = socket.gethostbyname(proxy_host)
1015 t1 = time.time()
1016 sysinfo['proxy_dns_latency'] = t1 - t0
1017 except socket_errors:
1018 pass
1019
1020 try:
1021 sock = socket.socket()
1022 t0 = time.time()
1023 sock.connect((proxy_ip or proxy_host, proxy_port))
1024 t1 = time.time()
1025 sysinfo['proxy_host_connect_latency'] = t1 - t0
1026 except socket_errors:
1027 pass
1028
760 # Try and find the number of CPUs in the system if available. 1029 # Try and find the number of CPUs in the system if available.
761 try: 1030 try:
762 sysinfo['cpu_count'] = multiprocessing.cpu_count() 1031 sysinfo['cpu_count'] = multiprocessing.cpu_count()
763 except NotImplementedError: 1032 except NotImplementedError:
764 sysinfo['cpu_count'] = None 1033 sysinfo['cpu_count'] = None
765 1034
766 # For *nix platforms, obtain the CPU load. 1035 # For *nix platforms, obtain the CPU load.
767 try: 1036 try:
768 sysinfo['load_avg'] = list(os.getloadavg()) 1037 sysinfo['load_avg'] = list(os.getloadavg())
769 except (AttributeError, OSError): 1038 except (AttributeError, OSError):
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after
895 print 'Read Throughput'.center(78) 1164 print 'Read Throughput'.center(78)
896 print '-' * 78 1165 print '-' * 78
897 read_thru = self.results['read_throughput'] 1166 read_thru = self.results['read_throughput']
898 print 'Copied a %s file %d times for a total transfer size of %s.' % ( 1167 print 'Copied a %s file %d times for a total transfer size of %s.' % (
899 MakeHumanReadable(read_thru['file_size']), 1168 MakeHumanReadable(read_thru['file_size']),
900 read_thru['num_times'], 1169 read_thru['num_times'],
901 MakeHumanReadable(read_thru['total_bytes_copied'])) 1170 MakeHumanReadable(read_thru['total_bytes_copied']))
902 print 'Read throughput: %s/s.' % ( 1171 print 'Read throughput: %s/s.' % (
903 MakeBitsHumanReadable(read_thru['bytes_per_second'] * 8)) 1172 MakeBitsHumanReadable(read_thru['bytes_per_second'] * 8))
904 1173
1174 if 'listing' in self.results:
1175 print
1176 print '-' * 78
1177 print 'Listing'.center(78)
1178 print '-' * 78
1179
1180 listing = self.results['listing']
1181 insert = listing['insert']
1182 delete = listing['delete']
1183 print 'After inserting %s objects:' % listing['num_files']
1184 print (' Total time for objects to appear: %.2g seconds' %
1185 insert['time_took'])
1186 print ' Number of listing calls made: %s' % insert['num_listing_calls']
1187 print (' Individual listing call latencies: [%s]' %
1188 ', '.join('%.2gs' % lat for lat in insert['list_latencies']))
1189 print (' Files reflected after each call: [%s]' %
1190 ', '.join(map(str, insert['files_seen_after_listing'])))
1191
1192 print 'After deleting %s objects:' % listing['num_files']
1193 print (' Total time for objects to appear: %.2g seconds' %
1194 delete['time_took'])
1195 print ' Number of listing calls made: %s' % delete['num_listing_calls']
1196 print (' Individual listing call latencies: [%s]' %
1197 ', '.join('%.2gs' % lat for lat in delete['list_latencies']))
1198 print (' Files reflected after each call: [%s]' %
1199 ', '.join(map(str, delete['files_seen_after_listing'])))
1200
905 if 'sysinfo' in self.results: 1201 if 'sysinfo' in self.results:
906 print 1202 print
907 print '-' * 78 1203 print '-' * 78
908 print 'System Information'.center(78) 1204 print 'System Information'.center(78)
909 print '-' * 78 1205 print '-' * 78
910 info = self.results['sysinfo'] 1206 info = self.results['sysinfo']
911 print 'IP Address: \n %s' % info['ip_address'] 1207 print 'IP Address: \n %s' % info['ip_address']
912 print 'Temporary Directory: \n %s' % info['tempdir'] 1208 print 'Temporary Directory: \n %s' % info['tempdir']
913 print 'Bucket URI: \n %s' % self.results['bucket_uri'] 1209 print 'Bucket URI: \n %s' % self.results['bucket_uri']
914 print 'gsutil Version: \n %s' % self.results.get('gsutil_version', 1210 print 'gsutil Version: \n %s' % self.results.get('gsutil_version',
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
982 print 1278 print
983 1279
984 if 'tcp_proc_values' in info: 1280 if 'tcp_proc_values' in info:
985 print 'TCP /proc values:\n', 1281 print 'TCP /proc values:\n',
986 for item in info['tcp_proc_values'].iteritems(): 1282 for item in info['tcp_proc_values'].iteritems():
987 print ' %s = %s' % item 1283 print ' %s = %s' % item
988 1284
989 if 'boto_https_enabled' in info: 1285 if 'boto_https_enabled' in info:
990 print 'Boto HTTPS Enabled: \n %s' % info['boto_https_enabled'] 1286 print 'Boto HTTPS Enabled: \n %s' % info['boto_https_enabled']
991 1287
1288 if 'using_proxy' in info:
1289 print 'Requests routed through proxy: \n %s' % info['using_proxy']
1290
1291 if 'google_host_dns_latency' in info:
1292 print ('Latency of the DNS lookup for Google Storage server (ms): '
1293 '\n %.1f' % (info['google_host_dns_latency'] * 1000.0))
1294
1295 if 'google_host_connect_latencies' in info:
1296 print 'Latencies connecting to Google Storage server IPs (ms):'
1297 for ip, latency in info['google_host_connect_latencies'].iteritems():
1298 print ' %s = %.1f' % (ip, latency * 1000.0)
1299
1300 if 'proxy_dns_latency' in info:
1301 print ('Latency of the DNS lookup for the configured proxy (ms): '
1302 '\n %.1f' % (info['proxy_dns_latency'] * 1000.0))
1303
1304 if 'proxy_host_connect_latency' in info:
1305 print ('Latency connecting to the configured proxy (ms): \n %.1f' %
1306 (info['proxy_host_connect_latency'] * 1000.0))
1307
992 if 'request_errors' in self.results and 'total_requests' in self.results: 1308 if 'request_errors' in self.results and 'total_requests' in self.results:
993 print 1309 print
994 print '-' * 78 1310 print '-' * 78
995 print 'In-Process HTTP Statistics'.center(78) 1311 print 'In-Process HTTP Statistics'.center(78)
996 print '-' * 78 1312 print '-' * 78
997 total = int(self.results['total_requests']) 1313 total = int(self.results['total_requests'])
998 numerrors = int(self.results['request_errors']) 1314 numerrors = int(self.results['request_errors'])
999 numbreaks = int(self.results['connection_breaks']) 1315 numbreaks = int(self.results['connection_breaks'])
1000 availability = (((total - numerrors) / float(total)) * 100 1316 availability = (((total - numerrors) / float(total)) * 100
1001 if total > 0 else 100) 1317 if total > 0 else 100)
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
1043 """Parses arguments for perfdiag command.""" 1359 """Parses arguments for perfdiag command."""
1044 # From -n. 1360 # From -n.
1045 self.num_iterations = 5 1361 self.num_iterations = 5
1046 # From -c. 1362 # From -c.
1047 self.processes = 1 1363 self.processes = 1
1048 # From -k. 1364 # From -k.
1049 self.threads = 1 1365 self.threads = 1
1050 # From -s. 1366 # From -s.
1051 self.thru_filesize = 1048576 1367 self.thru_filesize = 1048576
1052 # From -t. 1368 # From -t.
1053 self.diag_tests = self.ALL_DIAG_TESTS 1369 self.diag_tests = self.DEFAULT_DIAG_TESTS
1054 # From -o. 1370 # From -o.
1055 self.output_file = None 1371 self.output_file = None
1056 # From -i. 1372 # From -i.
1057 self.input_file = None 1373 self.input_file = None
1058 # From -m. 1374 # From -m.
1059 self.metadata_keys = {} 1375 self.metadata_keys = {}
1060 1376
1061 if self.sub_opts: 1377 if self.sub_opts:
1062 for o, a in self.sub_opts: 1378 for o, a in self.sub_opts:
1063 if o == '-n': 1379 if o == '-n':
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
1102 self.results = json.load(f) 1418 self.results = json.load(f)
1103 self.logger.info("Read input file: '%s'.", self.input_file) 1419 self.logger.info("Read input file: '%s'.", self.input_file)
1104 except ValueError: 1420 except ValueError:
1105 raise CommandException("Could not decode input file (-i): '%s'." % 1421 raise CommandException("Could not decode input file (-i): '%s'." %
1106 a) 1422 a)
1107 return 1423 return
1108 1424
1109 if not self.args: 1425 if not self.args:
1110 raise CommandException('Wrong number of arguments for "perfdiag" ' 1426 raise CommandException('Wrong number of arguments for "perfdiag" '
1111 'command.') 1427 'command.')
1112 self.bucket_uri = self.suri_builder.StorageUri(self.args[0]) 1428
1113 if not self.bucket_uri.names_bucket(): 1429 self.bucket_url = StorageUrlFromString(self.args[0])
1114 raise CommandException('The perfdiag command requires a URI that ' 1430 self.provider = self.bucket_url.scheme
1431 if not (self.bucket_url.IsCloudUrl() and self.bucket_url.IsBucket()):
1432 raise CommandException('The perfdiag command requires a URL that '
1115 'specifies a bucket.\n"%s" is not ' 1433 'specifies a bucket.\n"%s" is not '
1116 'valid.' % self.bucket_uri) 1434 'valid.' % self.args[0])
1117 self.bucket = self.bucket_uri.get_bucket() 1435 # Ensure the bucket exists.
1118 1436 self.gsutil_api.GetBucket(self.bucket_url.bucket_name,
1119 # TODO: Add MD5 argument support to get_contents_to_file() 1437 provider=self.bucket_url.scheme,
1120 # and pass the file md5 as a parameter to avoid any unnecessary 1438 fields=['id'])
1121 # computation. 1439 self.exceptions = [httplib.HTTPException, socket.error, socket.gaierror,
1122 self.get_contents_to_file_args = {} 1440 httplib.BadStatusLine, ServiceException]
1123 if self.bucket_uri.scheme == 'gs':
1124 self.get_contents_to_file_args = {'hash_algs': {}}
1125 1441
1126 # Command entry point. 1442 # Command entry point.
1127 def RunCommand(self): 1443 def RunCommand(self):
1128 """Called by gsutil when the command is being invoked.""" 1444 """Called by gsutil when the command is being invoked."""
1129 self._ParseArgs() 1445 self._ParseArgs()
1130 1446
1131 if self.input_file: 1447 if self.input_file:
1132 self._DisplayResults() 1448 self._DisplayResults()
1133 return 0 1449 return 0
1134 1450
1135 # We turn off retries in the underlying boto library because the 1451 # We turn off retries in the underlying boto library because the
1136 # _RunOperation function handles errors manually so it can count them. 1452 # _RunOperation function handles errors manually so it can count them.
1137 boto.config.set('Boto', 'num_retries', '0') 1453 boto.config.set('Boto', 'num_retries', '0')
1138 1454
1139 self.logger.info( 1455 self.logger.info(
1140 'Number of iterations to run: %d\n' 1456 'Number of iterations to run: %d\n'
1141 'Base bucket URI: %s\n' 1457 'Base bucket URI: %s\n'
1142 'Number of processes: %d\n' 1458 'Number of processes: %d\n'
1143 'Number of threads: %d\n' 1459 'Number of threads: %d\n'
1144 'Throughput file size: %s\n' 1460 'Throughput file size: %s\n'
1145 'Diagnostics to run: %s', 1461 'Diagnostics to run: %s',
1146 self.num_iterations, 1462 self.num_iterations,
1147 self.bucket_uri, 1463 self.bucket_url,
1148 self.processes, 1464 self.processes,
1149 self.threads, 1465 self.threads,
1150 MakeHumanReadable(self.thru_filesize), 1466 MakeHumanReadable(self.thru_filesize),
1151 (', '.join(self.diag_tests))) 1467 (', '.join(self.diag_tests)))
1152 1468
1153 try: 1469 try:
1154 self._SetUp() 1470 self._SetUp()
1155 1471
1156 # Collect generic system info. 1472 # Collect generic system info.
1157 self._CollectSysInfo() 1473 self._CollectSysInfo()
1158 # Collect netstat info and disk counters before tests (and again later). 1474 # Collect netstat info and disk counters before tests (and again later).
1159 self.results['sysinfo']['netstat_start'] = self._GetTcpStats() 1475 self.results['sysinfo']['netstat_start'] = self._GetTcpStats()
1160 if IS_LINUX: 1476 if IS_LINUX:
1161 self.results['sysinfo']['disk_counters_start'] = self._GetDiskCounters() 1477 self.results['sysinfo']['disk_counters_start'] = self._GetDiskCounters()
1162 # Record bucket URI. 1478 # Record bucket URL.
1163 self.results['bucket_uri'] = str(self.bucket_uri) 1479 self.results['bucket_uri'] = str(self.bucket_url)
1164 self.results['json_format'] = 'perfdiag' 1480 self.results['json_format'] = 'perfdiag'
1165 self.results['metadata'] = self.metadata_keys 1481 self.results['metadata'] = self.metadata_keys
1166 1482
1167 if 'lat' in self.diag_tests: 1483 if self.LAT in self.diag_tests:
1168 self._RunLatencyTests() 1484 self._RunLatencyTests()
1169 if 'rthru' in self.diag_tests: 1485 if self.RTHRU in self.diag_tests:
1170 self._RunReadThruTests() 1486 self._RunReadThruTests()
1171 if 'wthru' in self.diag_tests: 1487 if self.WTHRU in self.diag_tests:
1172 self._RunWriteThruTests() 1488 self._RunWriteThruTests()
1489 if self.LIST in self.diag_tests:
1490 self._RunListTests()
1173 1491
1174 # Collect netstat info and disk counters after tests. 1492 # Collect netstat info and disk counters after tests.
1175 self.results['sysinfo']['netstat_end'] = self._GetTcpStats() 1493 self.results['sysinfo']['netstat_end'] = self._GetTcpStats()
1176 if IS_LINUX: 1494 if IS_LINUX:
1177 self.results['sysinfo']['disk_counters_end'] = self._GetDiskCounters() 1495 self.results['sysinfo']['disk_counters_end'] = self._GetDiskCounters()
1178 1496
1179 self.results['total_requests'] = self.total_requests 1497 self.results['total_requests'] = self.total_requests
1180 self.results['request_errors'] = self.request_errors 1498 self.results['request_errors'] = self.request_errors
1181 self.results['error_responses_by_code'] = self.error_responses_by_code 1499 self.results['error_responses_by_code'] = self.error_responses_by_code
1182 self.results['connection_breaks'] = self.connection_breaks 1500 self.results['connection_breaks'] = self.connection_breaks
1183 self.results['gsutil_version'] = gslib.VERSION 1501 self.results['gsutil_version'] = gslib.VERSION
1184 self.results['boto_version'] = boto.__version__ 1502 self.results['boto_version'] = boto.__version__
1185 1503
1186 self._DisplayResults() 1504 self._DisplayResults()
1187 finally: 1505 finally:
1188 self._TearDown() 1506 self._TearDown()
1189 1507
1190 return 0 1508 return 0
1509
1510
1511 class UploadObjectTuple(object):
1512 """Picklable tuple with necessary metadata for an insert object call."""
1513
1514 def __init__(self, bucket_name, object_name, filepath=None, md5=None,
1515 contents=None):
1516 """Create an upload tuple.
1517
1518 Args:
1519 bucket_name: Name of the bucket to upload to.
1520 object_name: Name of the object to upload to.
1521 filepath: A file path located in self.file_contents and self.file_md5s.
1522 md5: The MD5 hash of the object being uploaded.
1523 contents: The contents of the file to be uploaded.
1524
1525 Note: (contents + md5) and filepath are mutually exlusive. You may specify
1526 one or the other, but not both.
1527 Note: If one of contents or md5 are specified, they must both be specified.
1528
1529 Raises:
1530 InvalidArgument: if the arguments are invalid.
1531 """
1532 self.bucket_name = bucket_name
1533 self.object_name = object_name
1534 self.filepath = filepath
1535 self.md5 = md5
1536 self.contents = contents
1537 if filepath and (md5 or contents is not None):
1538 raise InvalidArgument(
1539 'Only one of filepath or (md5 + contents) may be specified.')
1540 if not filepath and (not md5 or contents is None):
1541 raise InvalidArgument(
1542 'Both md5 and contents must be specified.')
1543
1544
1545 def StorageUrlToUploadObjectMetadata(storage_url):
1546 if storage_url.IsCloudUrl() and storage_url.IsObject():
1547 upload_target = apitools_messages.Object()
1548 upload_target.name = storage_url.object_name
1549 upload_target.bucket = storage_url.bucket_name
1550 return upload_target
1551 else:
1552 raise CommandException('Non-cloud URL upload target %s was created in '
1553 'perfdiag implemenation.' % storage_url)
OLDNEW
« no previous file with comments | « gslib/commands/notification.py ('k') | gslib/commands/rb.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698