| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # coding=utf8 | 2 # -*- coding: utf-8 -*- |
| 3 # Copyright 2013 Google Inc. All Rights Reserved. | 3 # Copyright 2013 Google Inc. All Rights Reserved. |
| 4 # | 4 # |
| 5 # Licensed under the Apache License, Version 2.0 (the "License"); | 5 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 # you may not use this file except in compliance with the License. | 6 # you may not use this file except in compliance with the License. |
| 7 # You may obtain a copy of the License at | 7 # You may obtain a copy of the License at |
| 8 # | 8 # |
| 9 # http://www.apache.org/licenses/LICENSE-2.0 | 9 # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 # | 10 # |
| 11 # Unless required by applicable law or agreed to in writing, software | 11 # Unless required by applicable law or agreed to in writing, software |
| 12 # distributed under the License is distributed on an "AS IS" BASIS, | 12 # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 # See the License for the specific language governing permissions and | 14 # See the License for the specific language governing permissions and |
| 15 # limitations under the License. | 15 # limitations under the License. |
| 16 | |
| 17 """Main module for Google Cloud Storage command line tool.""" | 16 """Main module for Google Cloud Storage command line tool.""" |
| 18 | 17 |
| 18 from __future__ import absolute_import |
| 19 |
| 19 import ConfigParser | 20 import ConfigParser |
| 21 import datetime |
| 20 import errno | 22 import errno |
| 21 import getopt | 23 import getopt |
| 22 import logging | 24 import logging |
| 23 import os | 25 import os |
| 24 import pkgutil | |
| 25 import re | 26 import re |
| 26 import signal | 27 import signal |
| 27 import socket | 28 import socket |
| 28 import sys | 29 import sys |
| 29 import tempfile | |
| 30 import textwrap | 30 import textwrap |
| 31 import traceback | 31 import traceback |
| 32 | 32 |
| 33 # Load the gsutil version number and append it to boto.UserAgent so the value is | 33 # Load the gsutil version number and append it to boto.UserAgent so the value is |
| 34 # set before anything instantiates boto. This has to run after THIRD_PARTY_DIR | 34 # set before anything instantiates boto. This has to run after THIRD_PARTY_DIR |
| 35 # is modified (done in gsutil.py) but before any calls are made that would cause | 35 # is modified (done in gsutil.py) but before any calls are made that would cause |
| 36 # boto.s3.Connection to be loaded - otherwise the Connection class would end up | 36 # boto.s3.Connection to be loaded - otherwise the Connection class would end up |
| 37 # with a static reference to the pre-modified version of the UserAgent field, | 37 # with a static reference to the pre-modified version of the UserAgent field, |
| 38 # so boto requests would not include gsutil/version# in the UserAgent string. | 38 # so boto requests would not include gsutil/version# in the UserAgent string. |
| 39 import boto | 39 import boto |
| 40 import gslib | 40 import gslib |
| 41 # TODO: gsutil-beta: Cloud SDK scans for this string and performs |
| 42 # substitution; ensure this works with both apitools and boto. |
| 41 boto.UserAgent += ' gsutil/%s (%s)' % (gslib.VERSION, sys.platform) | 43 boto.UserAgent += ' gsutil/%s (%s)' % (gslib.VERSION, sys.platform) |
| 42 | 44 |
| 43 import apiclient.discovery | 45 # pylint: disable=g-bad-import-order |
| 44 import boto.exception | 46 # pylint: disable=g-import-not-at-top |
| 45 from gslib import util | |
| 46 from gslib import GSUTIL_DIR | |
| 47 from gslib import wildcard_iterator | |
| 48 from gslib.command_runner import CommandRunner | |
| 49 from gslib.exception import CommandException | |
| 50 from gslib.util import BOTO_IS_SECURE | |
| 51 from gslib.util import GetBotoConfigFileList | |
| 52 from gslib.util import GetConfigFilePath | |
| 53 from gslib.util import HasConfiguredCredentials | |
| 54 from gslib.util import IsRunningInteractively | |
| 55 import gslib.exception | |
| 56 import httplib2 | 47 import httplib2 |
| 57 import oauth2client | 48 import oauth2client |
| 49 from gslib import wildcard_iterator |
| 50 from gslib.cloud_api import AccessDeniedException |
| 51 from gslib.cloud_api import ArgumentException |
| 52 from gslib.cloud_api import BadRequestException |
| 53 from gslib.cloud_api import ProjectIdException |
| 54 from gslib.cloud_api import ServiceException |
| 55 from gslib.command_runner import CommandRunner |
| 56 import gslib.exception |
| 57 from gslib.exception import CommandException |
| 58 import gslib.third_party.storage_apitools.exceptions as apitools_exceptions |
| 59 from gslib.util import CreateLock |
| 60 from gslib.util import GetBotoConfigFileList |
| 61 from gslib.util import GetCertsFile |
| 62 from gslib.util import GetCleanupFiles |
| 63 |
| 64 GSUTIL_CLIENT_ID = '909320924072.apps.googleusercontent.com' |
| 65 # Google OAuth2 clients always have a secret, even if the client is an installed |
| 66 # application/utility such as gsutil. Of course, in such cases the "secret" is |
| 67 # actually publicly known; security depends entirely on the secrecy of refresh |
| 68 # tokens, which effectively become bearer tokens. |
| 69 GSUTIL_CLIENT_NOTSOSECRET = 'p3RlpR10xMFh9ZXBS/ZNLYUu' |
| 70 |
| 71 CONFIG_KEYS_TO_REDACT = ['proxy', 'proxy_port', 'proxy_user', 'proxy_pass'] |
| 72 |
| 58 | 73 |
| 59 # We don't use the oauth2 authentication plugin directly; importing it here | 74 # We don't use the oauth2 authentication plugin directly; importing it here |
| 60 # ensures that it's loaded and available by default when an operation requiring | 75 # ensures that it's loaded and available by default when an operation requiring |
| 61 # authentication is performed. | 76 # authentication is performed. |
| 62 try: | 77 try: |
| 63 from gslib.third_party.oauth2_plugin import oauth2_plugin | 78 # pylint: disable=unused-import,g-import-not-at-top |
| 79 import gcs_oauth2_boto_plugin |
| 64 except ImportError: | 80 except ImportError: |
| 65 pass | 81 pass |
| 66 | 82 |
| 67 DEBUG_WARNING = """ | 83 DEBUG_WARNING = """ |
| 68 ***************************** WARNING ***************************** | 84 ***************************** WARNING ***************************** |
| 69 *** You are running gsutil with debug output enabled. | 85 *** You are running gsutil with debug output enabled. |
| 70 *** Be aware that debug output includes authentication credentials. | 86 *** Be aware that debug output includes authentication credentials. |
| 71 *** Make sure to remove the value of the Authorization header for | 87 *** Make sure to remove the value of the Authorization header for |
| 72 *** each HTTP request printed to the console prior to posting to | 88 *** each HTTP request printed to the console prior to posting to |
| 73 *** a public medium such as a forum post or Stack Overflow. | 89 *** a public medium such as a forum post or Stack Overflow. |
| 74 ***************************** WARNING ***************************** | 90 ***************************** WARNING ***************************** |
| 75 """.lstrip() | 91 """.lstrip() |
| 76 | 92 |
| 77 HTTP_WARNING = """ | 93 HTTP_WARNING = """ |
| 78 ***************************** WARNING ***************************** | 94 ***************************** WARNING ***************************** |
| 79 *** You are running gsutil with either the boto config variable "is_secure" set | 95 *** You are running gsutil with the "https_validate_certificates" config |
| 80 *** to False or the "https_validate_certificates" config variable set to False. | 96 *** variable set to False. This option should always be set to True in |
| 81 *** These options should always be set to True in production environments, to | 97 *** production environments to protect against man-in-the-middle attacks, |
| 82 *** protect against intercepted bearer tokens, man-in-the-middle attacks, and | 98 *** and leaking of user data. |
| 83 *** leaking of user data. | |
| 84 ***************************** WARNING ***************************** | 99 ***************************** WARNING ***************************** |
| 85 """.lstrip() | 100 """.lstrip() |
| 86 | 101 |
| 87 debug = 0 | 102 debug = 0 |
| 88 | 103 test_exception_traces = False |
| 89 # Temp files to delete, if possible, when program exits. | |
| 90 cleanup_files = [] | |
| 91 | 104 |
| 92 | 105 |
| 93 def _Cleanup(): | 106 def _Cleanup(): |
| 94 for fname in cleanup_files: | 107 for fname in GetCleanupFiles(): |
| 95 try: | 108 try: |
| 96 os.remove(fname) | 109 os.remove(fname) |
| 97 except OSError: | 110 except OSError: |
| 98 pass | 111 pass |
| 99 | 112 |
| 100 | 113 |
| 101 def _OutputAndExit(message): | 114 def _OutputAndExit(message): |
| 102 if debug == 4: | 115 """Outputs message and exists with code 1.""" |
| 116 from gslib.util import UTF8 # pylint: disable=g-import-not-at-top |
| 117 if debug >= 2 or test_exception_traces: |
| 103 stack_trace = traceback.format_exc() | 118 stack_trace = traceback.format_exc() |
| 104 err = ('DEBUG: Exception stack trace:\n %s\n' % | 119 err = ('DEBUG: Exception stack trace:\n %s\n' % |
| 105 re.sub('\\n', '\n ', stack_trace)) | 120 re.sub('\\n', '\n ', stack_trace)) |
| 106 else: | 121 else: |
| 107 err = '%s\n' % message | 122 err = '%s\n' % message |
| 108 sys.stderr.write(err.encode('utf-8')) | 123 try: |
| 124 sys.stderr.write(err.encode(UTF8)) |
| 125 except UnicodeDecodeError: |
| 126 # Can happen when outputting invalid Unicode filenames. |
| 127 sys.stderr.write(err) |
| 109 sys.exit(1) | 128 sys.exit(1) |
| 110 | 129 |
| 111 | 130 |
| 112 def _OutputUsageAndExit(command_runner): | 131 def _OutputUsageAndExit(command_runner): |
| 113 command_runner.RunNamedCommand('help') | 132 command_runner.RunNamedCommand('help') |
| 114 sys.exit(1) | 133 sys.exit(1) |
| 115 | 134 |
| 116 | 135 |
| 136 class GsutilFormatter(logging.Formatter): |
| 137 """A logging.Formatter that supports logging microseconds (%f).""" |
| 138 |
| 139 def formatTime(self, record, datefmt=None): |
| 140 if datefmt: |
| 141 return datetime.datetime.fromtimestamp(record.created).strftime(datefmt) |
| 142 |
| 143 # Use default implementation if datefmt is not specified. |
| 144 return super(GsutilFormatter, self).formatTime(record, datefmt=datefmt) |
| 145 |
| 146 |
| 117 def _ConfigureLogging(level=logging.INFO): | 147 def _ConfigureLogging(level=logging.INFO): |
| 118 """Similar to logging.basicConfig() except it always adds a handler.""" | 148 """Similar to logging.basicConfig() except it always adds a handler.""" |
| 149 log_format = '%(levelname)s %(asctime)s %(filename)s] %(message)s' |
| 150 date_format = '%m%d %H:%M:%S.%f' |
| 151 formatter = GsutilFormatter(fmt=log_format, datefmt=date_format) |
| 119 handler = logging.StreamHandler() | 152 handler = logging.StreamHandler() |
| 153 handler.setFormatter(formatter) |
| 120 root_logger = logging.getLogger() | 154 root_logger = logging.getLogger() |
| 121 root_logger.addHandler(handler) | 155 root_logger.addHandler(handler) |
| 122 root_logger.setLevel(level) | 156 root_logger.setLevel(level) |
| 123 | 157 |
| 124 | 158 |
| 125 def main(): | 159 def main(): |
| 126 # These modules must be imported after importing gslib.__main__. | 160 # Any modules used in initializing multiprocessing variables must be |
| 161 # imported after importing gslib.__main__. |
| 162 # pylint: disable=redefined-outer-name,g-import-not-at-top |
| 163 import gslib.boto_translation |
| 127 import gslib.command | 164 import gslib.command |
| 128 import gslib.util | 165 import gslib.util |
| 129 from gslib.third_party.oauth2_plugin import oauth2_client | 166 from gslib.util import BOTO_IS_SECURE |
| 167 from gslib.util import CERTIFICATE_VALIDATION_ENABLED |
| 168 from gcs_oauth2_boto_plugin import oauth2_client |
| 130 from gslib.util import MultiprocessingIsAvailable | 169 from gslib.util import MultiprocessingIsAvailable |
| 131 if MultiprocessingIsAvailable()[0]: | 170 if MultiprocessingIsAvailable()[0]: |
| 132 # These setup methods must be called, and, on Windows, they can only be | 171 # These setup methods must be called, and, on Windows, they can only be |
| 133 # called from within an "if __name__ == '__main__':" block. | 172 # called from within an "if __name__ == '__main__':" block. |
| 134 gslib.util.InitializeMultiprocessingVariables() | 173 gslib.util.InitializeMultiprocessingVariables() |
| 135 gslib.command.InitializeMultiprocessingVariables() | 174 gslib.command.InitializeMultiprocessingVariables() |
| 136 oauth2_client.InitializeMultiprocessingVariables() | 175 gslib.boto_translation.InitializeMultiprocessingVariables() |
| 176 |
| 177 # This needs to be done after gslib.util.InitializeMultiprocessingVariables(), |
| 178 # since otherwise we can't call gslib.util.CreateLock. |
| 179 try: |
| 180 # pylint: disable=unused-import,g-import-not-at-top |
| 181 import gcs_oauth2_boto_plugin |
| 182 gcs_oauth2_boto_plugin.oauth2_helper.SetFallbackClientIdAndSecret( |
| 183 GSUTIL_CLIENT_ID, GSUTIL_CLIENT_NOTSOSECRET) |
| 184 gcs_oauth2_boto_plugin.oauth2_helper.SetLock(CreateLock()) |
| 185 except ImportError: |
| 186 pass |
| 137 | 187 |
| 138 global debug | 188 global debug |
| 189 global test_exception_traces |
| 139 | 190 |
| 140 if not (2, 6) <= sys.version_info[:3] < (3,): | 191 if not (2, 6) <= sys.version_info[:3] < (3,): |
| 141 raise gslib.exception.CommandException( | 192 raise gslib.exception.CommandException( |
| 142 'gsutil requires python 2.6 or 2.7.') | 193 'gsutil requires python 2.6 or 2.7.') |
| 143 | 194 |
| 144 config_file_list = GetBotoConfigFileList() | 195 # In gsutil 4.0 and beyond, we don't use the boto library for the JSON |
| 145 command_runner = CommandRunner(config_file_list) | 196 # API. However, we still store gsutil configuration data in the .boto |
| 197 # config file for compatibility with previous versions and user convenience. |
| 198 # Many users have a .boto configuration file from previous versions, and it |
| 199 # is useful to have all of the configuration for gsutil stored in one place. |
| 200 command_runner = CommandRunner() |
| 201 if not BOTO_IS_SECURE: |
| 202 raise CommandException('\n'.join(textwrap.wrap( |
| 203 'Your boto configuration has is_secure = False. Gsutil cannot be ' |
| 204 'run this way, for security reasons.'))) |
| 205 |
| 146 headers = {} | 206 headers = {} |
| 147 parallel_operations = False | 207 parallel_operations = False |
| 148 quiet = False | 208 quiet = False |
| 149 version = False | 209 version = False |
| 150 debug = 0 | 210 debug = 0 |
| 211 test_exception_traces = False |
| 151 | 212 |
| 152 # If user enters no commands just print the usage info. | 213 # If user enters no commands just print the usage info. |
| 153 if len(sys.argv) == 1: | 214 if len(sys.argv) == 1: |
| 154 sys.argv.append('help') | 215 sys.argv.append('help') |
| 155 | 216 |
| 156 # Change the default of the 'https_validate_certificates' boto option to | 217 # Change the default of the 'https_validate_certificates' boto option to |
| 157 # True (it is currently False in boto). | 218 # True (it is currently False in boto). |
| 158 if not boto.config.has_option('Boto', 'https_validate_certificates'): | 219 if not boto.config.has_option('Boto', 'https_validate_certificates'): |
| 159 if not boto.config.has_section('Boto'): | 220 if not boto.config.has_section('Boto'): |
| 160 boto.config.add_section('Boto') | 221 boto.config.add_section('Boto') |
| 161 boto.config.setbool('Boto', 'https_validate_certificates', True) | 222 boto.config.setbool('Boto', 'https_validate_certificates', True) |
| 162 | 223 |
| 163 # If ca_certificates_file is configured use it; otherwise configure boto to | 224 GetCertsFile() |
| 164 # use the cert roots distributed with gsutil. | |
| 165 if not boto.config.get_value('Boto', 'ca_certificates_file', None): | |
| 166 disk_certs_file = os.path.abspath( | |
| 167 os.path.join(gslib.GSLIB_DIR, 'data', 'cacerts.txt')) | |
| 168 if not os.path.exists(disk_certs_file): | |
| 169 # If the file is not present on disk, this means the gslib module doesn't | |
| 170 # actually exist on disk anywhere. This can happen if it's being imported | |
| 171 # from a zip file. Unfortunately, we have to copy the certs file to a | |
| 172 # local temp file on disk because the underlying SSL socket requires it | |
| 173 # to be a filesystem path. | |
| 174 certs_data = pkgutil.get_data('gslib', 'data/cacerts.txt') | |
| 175 if not certs_data: | |
| 176 raise gslib.exception.CommandException( | |
| 177 'Certificates file not found. Please reinstall gsutil from scratch') | |
| 178 fd, fname = tempfile.mkstemp(suffix='.txt', prefix='gsutil-cacerts') | |
| 179 f = os.fdopen(fd, 'w') | |
| 180 f.write(certs_data) | |
| 181 f.close() | |
| 182 disk_certs_file = fname | |
| 183 cleanup_files.append(disk_certs_file) | |
| 184 boto.config.set('Boto', 'ca_certificates_file', disk_certs_file) | |
| 185 | 225 |
| 186 try: | 226 try: |
| 187 try: | 227 try: |
| 188 opts, args = getopt.getopt(sys.argv[1:], 'dDvo:h:mq', | 228 opts, args = getopt.getopt(sys.argv[1:], 'dDvo:h:mq', |
| 189 ['debug', 'detailedDebug', 'version', 'option', | 229 ['debug', 'detailedDebug', 'version', 'option', |
| 190 'help', 'header', 'multithreaded', 'quiet']) | 230 'help', 'header', 'multithreaded', 'quiet', |
| 231 'testexceptiontraces']) |
| 191 except getopt.GetoptError as e: | 232 except getopt.GetoptError as e: |
| 192 _HandleCommandException(gslib.exception.CommandException(e.msg)) | 233 _HandleCommandException(gslib.exception.CommandException(e.msg)) |
| 193 for o, a in opts: | 234 for o, a in opts: |
| 194 if o in ('-d', '--debug'): | 235 if o in ('-d', '--debug'): |
| 195 # Passing debug=2 causes boto to include httplib header output. | 236 # Passing debug=2 causes boto to include httplib header output. |
| 196 debug = 2 | 237 debug = 3 |
| 197 elif o in ('-D', '--detailedDebug'): | 238 elif o in ('-D', '--detailedDebug'): |
| 198 # We use debug level 3 to ask gsutil code to output more detailed | 239 # We use debug level 3 to ask gsutil code to output more detailed |
| 199 # debug output. This is a bit of a hack since it overloads the same | 240 # debug output. This is a bit of a hack since it overloads the same |
| 200 # flag that was originally implemented for boto use. And we use -DD | 241 # flag that was originally implemented for boto use. And we use -DD |
| 201 # to ask for really detailed debugging (i.e., including HTTP payload). | 242 # to ask for really detailed debugging (i.e., including HTTP payload). |
| 202 if debug == 3: | 243 if debug == 3: |
| 203 debug = 4 | 244 debug = 4 |
| 204 else: | 245 else: |
| 205 debug = 3 | 246 debug = 3 |
| 206 elif o in ('-?', '--help'): | 247 elif o in ('-?', '--help'): |
| 207 _OutputUsageAndExit(command_runner) | 248 _OutputUsageAndExit(command_runner) |
| 208 elif o in ('-h', '--header'): | 249 elif o in ('-h', '--header'): |
| 209 (hdr_name, _, hdr_val) = a.partition(':') | 250 (hdr_name, _, hdr_val) = a.partition(':') |
| 210 if not hdr_name: | 251 if not hdr_name: |
| 211 _OutputUsageAndExit(command_runner) | 252 _OutputUsageAndExit(command_runner) |
| 212 headers[hdr_name.lower()] = hdr_val | 253 headers[hdr_name.lower()] = hdr_val |
| 213 elif o in ('-m', '--multithreaded'): | 254 elif o in ('-m', '--multithreaded'): |
| 214 parallel_operations = True | 255 parallel_operations = True |
| 215 elif o in ('-q', '--quiet'): | 256 elif o in ('-q', '--quiet'): |
| 216 quiet = True | 257 quiet = True |
| 217 elif o in ('-v', '--version'): | 258 elif o in ('-v', '--version'): |
| 218 version = True | 259 version = True |
| 260 elif o == '--testexceptiontraces': # Hidden flag for integration tests. |
| 261 test_exception_traces = True |
| 219 elif o in ('-o', '--option'): | 262 elif o in ('-o', '--option'): |
| 220 (opt_section_name, _, opt_value) = a.partition('=') | 263 (opt_section_name, _, opt_value) = a.partition('=') |
| 221 if not opt_section_name: | 264 if not opt_section_name: |
| 222 _OutputUsageAndExit(command_runner) | 265 _OutputUsageAndExit(command_runner) |
| 223 (opt_section, _, opt_name) = opt_section_name.partition(':') | 266 (opt_section, _, opt_name) = opt_section_name.partition(':') |
| 224 if not opt_section or not opt_name: | 267 if not opt_section or not opt_name: |
| 225 _OutputUsageAndExit(command_runner) | 268 _OutputUsageAndExit(command_runner) |
| 226 if not boto.config.has_section(opt_section): | 269 if not boto.config.has_section(opt_section): |
| 227 boto.config.add_section(opt_section) | 270 boto.config.add_section(opt_section) |
| 228 boto.config.set(opt_section, opt_name, opt_value) | 271 boto.config.set(opt_section, opt_name, opt_value) |
| 229 httplib2.debuglevel = debug | 272 httplib2.debuglevel = debug |
| 230 if debug > 1: | 273 if debug > 1: |
| 231 sys.stderr.write(DEBUG_WARNING) | 274 sys.stderr.write(DEBUG_WARNING) |
| 232 if debug == 2: | 275 if debug >= 2: |
| 233 _ConfigureLogging(level=logging.DEBUG) | |
| 234 elif debug > 2: | |
| 235 _ConfigureLogging(level=logging.DEBUG) | 276 _ConfigureLogging(level=logging.DEBUG) |
| 236 command_runner.RunNamedCommand('ver', ['-l']) | 277 command_runner.RunNamedCommand('ver', ['-l']) |
| 237 config_items = [] | 278 config_items = [] |
| 238 try: | 279 try: |
| 239 config_items.extend(boto.config.items('Boto')) | 280 config_items.extend(boto.config.items('Boto')) |
| 240 config_items.extend(boto.config.items('GSUtil')) | 281 config_items.extend(boto.config.items('GSUtil')) |
| 241 except ConfigParser.NoSectionError: | 282 except ConfigParser.NoSectionError: |
| 242 pass | 283 pass |
| 243 sys.stderr.write('config_file_list: %s\n' % config_file_list) | 284 for i in xrange(len(config_items)): |
| 285 config_item_key = config_items[i][0] |
| 286 if config_item_key in CONFIG_KEYS_TO_REDACT: |
| 287 config_items[i] = (config_item_key, 'REDACTED') |
| 288 sys.stderr.write('Command being run: %s\n' % ' '.join(sys.argv)) |
| 289 sys.stderr.write('config_file_list: %s\n' % GetBotoConfigFileList()) |
| 244 sys.stderr.write('config: %s\n' % str(config_items)) | 290 sys.stderr.write('config: %s\n' % str(config_items)) |
| 245 elif quiet: | 291 elif quiet: |
| 246 _ConfigureLogging(level=logging.WARNING) | 292 _ConfigureLogging(level=logging.WARNING) |
| 247 else: | 293 else: |
| 248 _ConfigureLogging(level=logging.INFO) | 294 _ConfigureLogging(level=logging.INFO) |
| 249 # apiclient and oauth2client use info logging in places that would better | 295 # apiclient and oauth2client use info logging in places that would better |
| 250 # correspond to gsutil's debug logging (e.g., when refreshing access | 296 # correspond to gsutil's debug logging (e.g., when refreshing access |
| 251 # tokens). | 297 # tokens). |
| 252 oauth2client.client.logger.setLevel(logging.WARNING) | 298 oauth2client.client.logger.setLevel(logging.WARNING) |
| 253 apiclient.discovery.logger.setLevel(logging.WARNING) | |
| 254 | 299 |
| 255 is_secure = BOTO_IS_SECURE | 300 if not CERTIFICATE_VALIDATION_ENABLED: |
| 256 if not is_secure[0]: | |
| 257 sys.stderr.write(HTTP_WARNING) | 301 sys.stderr.write(HTTP_WARNING) |
| 258 | 302 |
| 259 if version: | 303 if version: |
| 260 command_name = 'version' | 304 command_name = 'version' |
| 261 elif not args: | 305 elif not args: |
| 262 command_name = 'help' | 306 command_name = 'help' |
| 263 else: | 307 else: |
| 264 command_name = args[0] | 308 command_name = args[0] |
| 265 | 309 |
| 266 # Unset http_proxy environment variable if it's set, because it confuses | 310 # Unset http_proxy environment variable if it's set, because it confuses |
| 267 # boto. (Proxies should instead be configured via the boto config file.) | 311 # boto. (Proxies should instead be configured via the boto config file.) |
| 268 if 'http_proxy' in os.environ: | 312 if 'http_proxy' in os.environ: |
| 269 if debug > 1: | 313 if debug > 1: |
| 270 sys.stderr.write( | 314 sys.stderr.write( |
| 271 'Unsetting http_proxy environment variable within gsutil run.\n') | 315 'Unsetting http_proxy environment variable within gsutil run.\n') |
| 272 del os.environ['http_proxy'] | 316 del os.environ['http_proxy'] |
| 273 | 317 |
| 274 return _RunNamedCommandAndHandleExceptions( | 318 return _RunNamedCommandAndHandleExceptions( |
| 275 command_runner, command_name, args[1:], headers, debug, | 319 command_runner, command_name, args=args[1:], headers=headers, |
| 276 parallel_operations) | 320 debug_level=debug, parallel_operations=parallel_operations) |
| 277 finally: | 321 finally: |
| 278 _Cleanup() | 322 _Cleanup() |
| 279 | 323 |
| 280 | 324 |
| 281 def _HandleUnknownFailure(e): | 325 def _HandleUnknownFailure(e): |
| 282 # Called if we fall through all known/handled exceptions. Allows us to | 326 # Called if we fall through all known/handled exceptions. Allows us to |
| 283 # print a stacktrace if -D option used. | 327 # print a stacktrace if -D option used. |
| 284 if debug > 2: | 328 if debug >= 2: |
| 285 stack_trace = traceback.format_exc() | 329 stack_trace = traceback.format_exc() |
| 286 sys.stderr.write('DEBUG: Exception stack trace:\n %s\n' % | 330 sys.stderr.write('DEBUG: Exception stack trace:\n %s\n' % |
| 287 re.sub('\\n', '\n ', stack_trace)) | 331 re.sub('\\n', '\n ', stack_trace)) |
| 288 else: | 332 else: |
| 289 _OutputAndExit('Failure: %s.' % e) | 333 _OutputAndExit('Failure: %s.' % e) |
| 290 | 334 |
| 291 | 335 |
| 292 def _HandleCommandException(e): | 336 def _HandleCommandException(e): |
| 293 if e.informational: | 337 if e.informational: |
| 294 _OutputAndExit(e.reason) | 338 _OutputAndExit(e.reason) |
| 295 else: | 339 else: |
| 296 _OutputAndExit('CommandException: %s' % e.reason) | 340 _OutputAndExit('CommandException: %s' % e.reason) |
| 297 | 341 |
| 298 | 342 |
| 343 # pylint: disable=unused-argument |
| 299 def _HandleControlC(signal_num, cur_stack_frame): | 344 def _HandleControlC(signal_num, cur_stack_frame): |
| 300 """Called when user hits ^C so we can print a brief message instead of | 345 """Called when user hits ^C. |
| 301 the normal Python stack trace (unless -D option is used).""" | 346 |
| 302 if debug > 2: | 347 This function prints a brief message instead of the normal Python stack trace |
| 348 (unless -D option is used). |
| 349 |
| 350 Args: |
| 351 signal_num: Signal that was caught. |
| 352 cur_stack_frame: Unused. |
| 353 """ |
| 354 if debug >= 2: |
| 303 stack_trace = ''.join(traceback.format_list(traceback.extract_stack())) | 355 stack_trace = ''.join(traceback.format_list(traceback.extract_stack())) |
| 304 _OutputAndExit( | 356 _OutputAndExit( |
| 305 'DEBUG: Caught signal %d - Exception stack trace:\n' | 357 'DEBUG: Caught signal %d - Exception stack trace:\n' |
| 306 ' %s' % (signal_num, re.sub('\\n', '\n ', stack_trace))) | 358 ' %s' % (signal_num, re.sub('\\n', '\n ', stack_trace))) |
| 307 else: | 359 else: |
| 308 _OutputAndExit('Caught signal %d - exiting' % signal_num) | 360 _OutputAndExit('Caught signal %d - exiting' % signal_num) |
| 309 | 361 |
| 310 | 362 |
| 311 def _HandleSigQuit(signal_num, cur_stack_frame): | 363 def _HandleSigQuit(signal_num, cur_stack_frame): |
| 312 """Called when user hits ^\\, so we can force breakpoint a running gsutil.""" | 364 """Called when user hits ^\\, so we can force breakpoint a running gsutil.""" |
| 313 import pdb | 365 import pdb # pylint: disable=g-import-not-at-top |
| 314 pdb.set_trace() | 366 pdb.set_trace() |
| 315 | 367 |
| 316 def _ConstructAclHelp(default_project_id): | |
| 317 acct_help_part_1 = ( | |
| 318 """Your request resulted in an AccountProblem (403) error. Usually this happens | |
| 319 if you attempt to create a bucket or upload an object without having first | |
| 320 enabled billing for the project you are using. To remedy this problem, please do | |
| 321 the following: | |
| 322 | 368 |
| 323 1. Navigate to the https://cloud.google.com/console#/project, click on the | 369 def _ConstructAccountProblemHelp(reason): |
| 324 project you will use, and then copy the Project Number listed under that | 370 """Constructs a help string for an access control error. |
| 325 project. | |
| 326 | 371 |
| 327 """) | 372 Args: |
| 328 acct_help_part_2 = '\n' | 373 reason: e.reason string from caught exception. |
| 374 |
| 375 Returns: |
| 376 Contructed help text. |
| 377 """ |
| 378 default_project_id = boto.config.get_value('GSUtil', 'default_project_id') |
| 379 # pylint: disable=line-too-long, g-inconsistent-quotes |
| 380 acct_help = ( |
| 381 "Your request resulted in an AccountProblem (403) error. Usually this " |
| 382 "happens if you attempt to create a bucket without first having " |
| 383 "enabled billing for the project you are using. Please ensure billing is " |
| 384 "enabled for your project by following the instructions at " |
| 385 "`Google Developers Console<https://developers.google.com/console/help/bil
ling>`. ") |
| 329 if default_project_id: | 386 if default_project_id: |
| 330 acct_help_part_2 = ( | 387 acct_help += ( |
| 331 """2. Click "Google Cloud Storage" on the left hand pane, and then check that | 388 "In the project overview, ensure that the Project Number listed for " |
| 332 the value listed for "x-goog-project-id" on this page matches the project ID | 389 "your project matches the project ID (%s) from your boto config file. " |
| 333 (%s) from your boto config file. | 390 % default_project_id) |
| 391 acct_help += ( |
| 392 "If the above doesn't resolve your AccountProblem, please send mail to " |
| 393 "gs-team@google.com requesting assistance, noting the exact command you " |
| 394 "ran, the fact that you received a 403 AccountProblem error, and your " |
| 395 "project ID. Please do not post your project ID on StackOverflow. " |
| 396 "Note: It's possible to use Google Cloud Storage without enabling " |
| 397 "billing if you're only listing or reading objects for which you're " |
| 398 "authorized, or if you're uploading objects to a bucket billed to a " |
| 399 "project that has billing enabled. But if you're attempting to create " |
| 400 "buckets or upload objects to a bucket owned by your own project, you " |
| 401 "must first enable billing for that project.") |
| 402 return acct_help |
| 334 | 403 |
| 335 """ % default_project_id) | |
| 336 acct_help_part_3 = ( | |
| 337 """Check whether there's an "!" next to Billing. If so, click Billing and then | |
| 338 enable billing for this project. Note that it can take up to one hour after | |
| 339 enabling billing for the project to become activated for creating buckets and | |
| 340 uploading objects. | |
| 341 | 404 |
| 342 If the above doesn't resolve your AccountProblem, please send mail to | 405 def _CheckAndHandleCredentialException(e, args): |
| 343 gs-team@google.com requesting assistance, noting the exact command you ran, the | 406 # Provide detail to users who have no boto config file (who might previously |
| 344 fact that you received a 403 AccountProblem error, and your project ID. Please | 407 # have been using gsutil only for accessing publicly readable buckets and |
| 345 do not post your project ID on StackOverflow. | 408 # objects). |
| 409 # pylint: disable=g-import-not-at-top |
| 410 from gslib.util import HasConfiguredCredentials |
| 411 if (not HasConfiguredCredentials() and |
| 412 not boto.config.get_value('Tests', 'bypass_anonymous_access_warning', |
| 413 False)): |
| 414 # The check above allows tests to assert that we get a particular, |
| 415 # expected failure, rather than always encountering this error message |
| 416 # when there are no configured credentials. This allows tests to |
| 417 # simulate a second user without permissions, without actually requiring |
| 418 # two separate configured users. |
| 419 _OutputAndExit('\n'.join(textwrap.wrap( |
| 420 'You are attempting to access protected data with no configured ' |
| 421 'credentials. Please visit ' |
| 422 'https://cloud.google.com/console#/project and sign up for an ' |
| 423 'account, and then run the "gsutil config" command to configure ' |
| 424 'gsutil to use these credentials.'))) |
| 425 elif (e.reason and |
| 426 (e.reason == 'AccountProblem' or e.reason == 'Account disabled.' or |
| 427 'account for the specified project has been disabled' in e.reason) |
| 428 and ','.join(args).find('gs://') != -1): |
| 429 _OutputAndExit('\n'.join(textwrap.wrap( |
| 430 _ConstructAccountProblemHelp(e.reason)))) |
| 346 | 431 |
| 347 Note: It's possible to use Google Cloud Storage without enabling billing if | |
| 348 you're only listing or reading objects for which you're authorized, or if | |
| 349 you're uploading objects to a bucket billed to a project that has billing | |
| 350 enabled. But if you're attempting to create buckets or upload objects to a | |
| 351 bucket owned by your own project, you must first enable billing for that | |
| 352 project.""") | |
| 353 return (acct_help_part_1, acct_help_part_2, acct_help_part_3) | |
| 354 | 432 |
| 355 def _RunNamedCommandAndHandleExceptions(command_runner, command_name, args=None, | 433 def _RunNamedCommandAndHandleExceptions(command_runner, command_name, args=None, |
| 356 headers=None, debug=0, | 434 headers=None, debug_level=0, |
| 357 parallel_operations=False): | 435 parallel_operations=False): |
| 436 """Runs the command with the given command runner and arguments.""" |
| 437 # pylint: disable=g-import-not-at-top |
| 438 from gslib.util import GetConfigFilePath |
| 439 from gslib.util import IS_WINDOWS |
| 440 from gslib.util import IsRunningInteractively |
| 358 try: | 441 try: |
| 359 # Catch ^C so we can print a brief message instead of the normal Python | 442 # Catch ^C so we can print a brief message instead of the normal Python |
| 360 # stack trace. | 443 # stack trace. |
| 361 signal.signal(signal.SIGINT, _HandleControlC) | 444 signal.signal(signal.SIGINT, _HandleControlC) |
| 362 # Catch ^\ so we can force a breakpoint in a running gsutil. | 445 # Catch ^\ so we can force a breakpoint in a running gsutil. |
| 363 if not util.IS_WINDOWS: | 446 if not IS_WINDOWS: |
| 364 signal.signal(signal.SIGQUIT, _HandleSigQuit) | 447 signal.signal(signal.SIGQUIT, _HandleSigQuit) |
| 365 return command_runner.RunNamedCommand(command_name, args, headers, debug, | 448 return command_runner.RunNamedCommand(command_name, args, headers, |
| 366 parallel_operations) | 449 debug_level, parallel_operations) |
| 367 except AttributeError as e: | 450 except AttributeError as e: |
| 368 if str(e).find('secret_access_key') != -1: | 451 if str(e).find('secret_access_key') != -1: |
| 369 _OutputAndExit('Missing credentials for the given URI(s). Does your ' | 452 _OutputAndExit('Missing credentials for the given URI(s). Does your ' |
| 370 'boto config file contain all needed credentials?') | 453 'boto config file contain all needed credentials?') |
| 371 else: | 454 else: |
| 372 _OutputAndExit(str(e)) | 455 _OutputAndExit(str(e)) |
| 373 except boto.exception.StorageDataError as e: | |
| 374 _OutputAndExit('StorageDataError: %s.' % e.reason) | |
| 375 except boto.exception.BotoClientError as e: | |
| 376 _OutputAndExit('BotoClientError: %s.' % e.reason) | |
| 377 except gslib.exception.CommandException as e: | 456 except gslib.exception.CommandException as e: |
| 378 _HandleCommandException(e) | 457 _HandleCommandException(e) |
| 379 except getopt.GetoptError as e: | 458 except getopt.GetoptError as e: |
| 380 _HandleCommandException(gslib.exception.CommandException(e.msg)) | 459 _HandleCommandException(gslib.exception.CommandException(e.msg)) |
| 381 except boto.exception.InvalidAclError as e: | |
| 382 _OutputAndExit('InvalidAclError: %s.' % str(e)) | |
| 383 except boto.exception.InvalidUriError as e: | 460 except boto.exception.InvalidUriError as e: |
| 384 _OutputAndExit('InvalidUriError: %s.' % e.message) | 461 _OutputAndExit('InvalidUriError: %s.' % e.message) |
| 385 except gslib.exception.ProjectIdException as e: | 462 except gslib.exception.InvalidUrlError as e: |
| 386 _OutputAndExit('ProjectIdException: %s.' % e.reason) | 463 _OutputAndExit('InvalidUrlError: %s.' % e.message) |
| 387 except boto.auth_handler.NotReadyToAuthenticate: | 464 except boto.auth_handler.NotReadyToAuthenticate: |
| 388 _OutputAndExit('NotReadyToAuthenticate') | 465 _OutputAndExit('NotReadyToAuthenticate') |
| 389 except OSError as e: | 466 except OSError as e: |
| 390 _OutputAndExit('OSError: %s.' % e.strerror) | 467 _OutputAndExit('OSError: %s.' % e.strerror) |
| 391 except IOError as e: | 468 except IOError as e: |
| 392 if e.errno == errno.EPIPE and not IsRunningInteractively(): | 469 if (e.errno == errno.EPIPE or (IS_WINDOWS and e.errno == errno.EINVAL) |
| 470 and not IsRunningInteractively()): |
| 393 # If we get a pipe error, this just means that the pipe to stdout or | 471 # If we get a pipe error, this just means that the pipe to stdout or |
| 394 # stderr is broken. This can happen if the user pipes gsutil to a command | 472 # stderr is broken. This can happen if the user pipes gsutil to a command |
| 395 # that doesn't use the entire output stream. Instead of raising an error, | 473 # that doesn't use the entire output stream. Instead of raising an error, |
| 396 # just swallow it up and exit cleanly. | 474 # just swallow it up and exit cleanly. |
| 397 sys.exit(0) | 475 sys.exit(0) |
| 398 else: | 476 else: |
| 399 raise | 477 raise |
| 400 except wildcard_iterator.WildcardException as e: | 478 except wildcard_iterator.WildcardException as e: |
| 401 _OutputAndExit(e.reason) | 479 _OutputAndExit(e.reason) |
| 402 except boto.exception.StorageResponseError as e: | 480 except ProjectIdException as e: |
| 403 # Check for access denied, and provide detail to users who have no boto | 481 _OutputAndExit( |
| 404 # config file (who might previously have been using gsutil only for | 482 'You are attempting to perform an operation that requires a ' |
| 405 # accessing publicly readable buckets and objects). | 483 'project id, with none configured. Please re-run ' |
| 406 if (e.status == 403 | 484 'gsutil config and make sure to follow the instructions for ' |
| 407 or (e.status == 400 and e.code == 'MissingSecurityHeader')): | 485 'finding and entering your default project id.') |
| 408 _, _, detail = util.ParseErrorDetail(e) | 486 except BadRequestException as e: |
| 409 if detail and detail.find('x-goog-project-id header is required') != -1: | 487 if e.reason == 'MissingSecurityHeader': |
| 410 _OutputAndExit('\n'.join(textwrap.wrap( | 488 _CheckAndHandleCredentialException(e, args) |
| 411 'You are attempting to perform an operation that requires an ' | 489 _OutputAndExit(e) |
| 412 'x-goog-project-id header, with none configured. Please re-run ' | 490 except AccessDeniedException as e: |
| 413 'gsutil config and make sure to follow the instructions for ' | 491 _CheckAndHandleCredentialException(e, args) |
| 414 'finding and entering your default project id.'))) | 492 _OutputAndExit(e) |
| 415 if (not HasConfiguredCredentials() and | 493 except ArgumentException as e: |
| 416 not boto.config.get_value('Tests', 'bypass_anonymous_access_warning', | 494 _OutputAndExit(e) |
| 417 False)): | 495 except ServiceException as e: |
| 418 # The check above allows tests to assert that we get a particular, | 496 _OutputAndExit(e) |
| 419 # expected failure, rather than always encountering this error message | 497 except apitools_exceptions.HttpError as e: |
| 420 # when there are no configured credentials. This allows tests to | 498 # These should usually be retried by the underlying implementation or |
| 421 # simulate a second user without permissions, without actually requiring | 499 # wrapped by CloudApi ServiceExceptions, but if we do get them, |
| 422 # two separate configured users. | 500 # print something useful. |
| 423 _OutputAndExit('\n'.join(textwrap.wrap( | 501 _OutputAndExit('HttpError: %s, %s' % (getattr(e.response, 'status', ''), |
| 424 'You are attempting to access protected data with no configured ' | 502 e.content or '')) |
| 425 'credentials. Please visit ' | |
| 426 'https://cloud.google.com/console#/project and sign up for an ' | |
| 427 'account, and then run the "gsutil config" command to configure ' | |
| 428 'gsutil to use these credentials.'))) | |
| 429 elif (e.error_code == 'AccountProblem' | |
| 430 and ','.join(args).find('gs://') != -1): | |
| 431 default_project_id = boto.config.get_value('GSUtil', | |
| 432 'default_project_id') | |
| 433 (acct_help_part_1, acct_help_part_2, acct_help_part_3) = ( | |
| 434 _ConstructAclHelp(default_project_id)) | |
| 435 if default_project_id: | |
| 436 _OutputAndExit(acct_help_part_1 + acct_help_part_2 + '3. ' + | |
| 437 acct_help_part_3) | |
| 438 else: | |
| 439 _OutputAndExit(acct_help_part_1 + '2. ' + acct_help_part_3) | |
| 440 | |
| 441 exc_name, message, detail = util.ParseErrorDetail(e) | |
| 442 _OutputAndExit(util.FormatErrorMessage( | |
| 443 exc_name, e.status, e.code, e.reason, message, detail)) | |
| 444 except boto.exception.ResumableUploadException as e: | |
| 445 _OutputAndExit('ResumableUploadException: %s.' % e.message) | |
| 446 except socket.error as e: | 503 except socket.error as e: |
| 447 if e.args[0] == errno.EPIPE: | 504 if e.args[0] == errno.EPIPE: |
| 448 # Retrying with a smaller file (per suggestion below) works because | 505 # Retrying with a smaller file (per suggestion below) works because |
| 449 # the library code send loop (in boto/s3/key.py) can get through the | 506 # the library code send loop (in boto/s3/key.py) can get through the |
| 450 # entire file and then request the HTTP response before the socket | 507 # entire file and then request the HTTP response before the socket |
| 451 # gets closed and the response lost. | 508 # gets closed and the response lost. |
| 452 message = ( | 509 _OutputAndExit( |
| 453 """ | 510 'Got a "Broken pipe" error. This can happen to clients using Python ' |
| 454 Got a "Broken pipe" error. This can happen to clients using Python 2.x, | 511 '2.x, when the server sends an error response and then closes the ' |
| 455 when the server sends an error response and then closes the socket (see | 512 'socket (see http://bugs.python.org/issue5542). If you are trying to ' |
| 456 http://bugs.python.org/issue5542). If you are trying to upload a large | 513 'upload a large object you might retry with a small (say 200k) ' |
| 457 object you might retry with a small (say 200k) object, and see if you get | 514 'object, and see if you get a more specific error code.' |
| 458 a more specific error code. | 515 ) |
| 459 """) | |
| 460 _OutputAndExit(message) | |
| 461 else: | 516 else: |
| 462 _HandleUnknownFailure(e) | 517 _HandleUnknownFailure(e) |
| 463 except Exception as e: | 518 except Exception as e: |
| 464 # Check for two types of errors related to service accounts. These errors | 519 # Check for two types of errors related to service accounts. These errors |
| 465 # appear to be the same except for their messages, but they are caused by | 520 # appear to be the same except for their messages, but they are caused by |
| 466 # different problems and both have unhelpful error messages. Moreover, | 521 # different problems and both have unhelpful error messages. Moreover, |
| 467 # the error type belongs to PyOpenSSL, which is not necessarily installed. | 522 # the error type belongs to PyOpenSSL, which is not necessarily installed. |
| 468 if 'mac verify failure' in str(e): | 523 if 'mac verify failure' in str(e): |
| 469 _OutputAndExit("Encountered an error while refreshing access token." + | 524 _OutputAndExit( |
| 470 " If you are using a service account,\nplease verify that the " + | 525 'Encountered an error while refreshing access token. ' |
| 471 "gs_service_key_file_password field in your config file," + | 526 'If you are using a service account,\nplease verify that the ' |
| 472 "\n%s, is correct." % GetConfigFilePath()) | 527 'gs_service_key_file_password field in your config file,' |
| 528 '\n%s, is correct.' % GetConfigFilePath()) |
| 473 elif 'asn1 encoding routines' in str(e): | 529 elif 'asn1 encoding routines' in str(e): |
| 474 _OutputAndExit("Encountered an error while refreshing access token." + | 530 _OutputAndExit( |
| 475 " If you are using a service account,\nplease verify that the " + | 531 'Encountered an error while refreshing access token. ' |
| 476 "gs_service_key_file field in your config file,\n%s, is correct." | 532 'If you are using a service account,\nplease verify that the ' |
| 533 'gs_service_key_file field in your config file,\n%s, is correct.' |
| 477 % GetConfigFilePath()) | 534 % GetConfigFilePath()) |
| 478 _HandleUnknownFailure(e) | 535 _HandleUnknownFailure(e) |
| 479 | 536 |
| 480 | 537 |
| 481 if __name__ == '__main__': | 538 if __name__ == '__main__': |
| 482 sys.exit(main()) | 539 sys.exit(main()) |
| OLD | NEW |