Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / tools / telemetry / third_party / gsutilz / gslib / command_runner.py
blobe3f4bda25175513902caae26b2f2517bc84f2be6
1 # -*- coding: utf-8 -*-
2 # Copyright 2011 Google Inc. All Rights Reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Class that runs a named gsutil command."""
17 from __future__ import absolute_import
19 import difflib
20 import logging
21 import os
22 import pkgutil
23 import sys
24 import textwrap
25 import time
27 import boto
28 from boto.storage_uri import BucketStorageUri
29 import gslib
30 from gslib.cloud_api_delegator import CloudApiDelegator
31 from gslib.command import Command
32 from gslib.command import CreateGsutilLogger
33 from gslib.command import GetFailureCount
34 from gslib.command import OLD_ALIAS_MAP
35 from gslib.command import ShutDownGsutil
36 import gslib.commands
37 from gslib.cs_api_map import ApiSelector
38 from gslib.cs_api_map import GsutilApiClassMapFactory
39 from gslib.cs_api_map import GsutilApiMapFactory
40 from gslib.exception import CommandException
41 from gslib.gcs_json_api import GcsJsonApi
42 from gslib.no_op_credentials import NoOpCredentials
43 from gslib.tab_complete import MakeCompleter
44 from gslib.util import CompareVersions
45 from gslib.util import GetGsutilVersionModifiedTime
46 from gslib.util import GSUTIL_PUB_TARBALL
47 from gslib.util import IsRunningInteractively
48 from gslib.util import LAST_CHECKED_FOR_GSUTIL_UPDATE_TIMESTAMP_FILE
49 from gslib.util import LookUpGsutilVersion
50 from gslib.util import MultiprocessingIsAvailable
51 from gslib.util import RELEASE_NOTES_URL
52 from gslib.util import SECONDS_PER_DAY
53 from gslib.util import UTF8
56 def HandleArgCoding(args):
57 """Handles coding of command-line args.
59 Args:
60 args: array of command-line args.
62 Returns:
63 array of command-line args.
65 Raises:
66 CommandException: if errors encountered.
67 """
68 # Python passes arguments from the command line as byte strings. To
69 # correctly interpret them, we decode ones other than -h and -p args (which
70 # will be passed as headers, and thus per HTTP spec should not be encoded) as
71 # utf-8. The exception is x-goog-meta-* headers, which are allowed to contain
72 # non-ASCII content (and hence, should be decoded), per
73 # https://developers.google.com/storage/docs/gsutil/addlhelp/WorkingWithObjectMetadata
74 processing_header = False
75 for i in range(len(args)):
76 arg = args[i]
77 # Commands like mv can run this function twice; don't decode twice.
78 try:
79 decoded = arg if isinstance(arg, unicode) else arg.decode(UTF8)
80 except UnicodeDecodeError:
81 raise CommandException('\n'.join(textwrap.wrap(
82 'Invalid encoding for argument (%s). Arguments must be decodable as '
83 'Unicode. NOTE: the argument printed above replaces the problematic '
84 'characters with a hex-encoded printable representation. For more '
85 'details (including how to convert to a gsutil-compatible encoding) '
86 'see `gsutil help encoding`.' % repr(arg))))
87 if processing_header:
88 if arg.lower().startswith('x-goog-meta'):
89 args[i] = decoded
90 else:
91 try:
92 # Try to encode as ASCII to check for invalid header values (which
93 # can't be sent over HTTP).
94 decoded.encode('ascii')
95 except UnicodeEncodeError:
96 # Raise the CommandException using the decoded value because
97 # _OutputAndExit function re-encodes at the end.
98 raise CommandException(
99 'Invalid non-ASCII header value (%s).\nOnly ASCII characters are '
100 'allowed in headers other than x-goog-meta- headers' % decoded)
101 else:
102 args[i] = decoded
103 processing_header = (arg in ('-h', '-p'))
104 return args
107 class CommandRunner(object):
108 """Runs gsutil commands and does some top-level argument handling."""
110 def __init__(self, bucket_storage_uri_class=BucketStorageUri,
111 gsutil_api_class_map_factory=GsutilApiClassMapFactory,
112 command_map=None):
113 """Instantiates a CommandRunner.
115 Args:
116 bucket_storage_uri_class: Class to instantiate for cloud StorageUris.
117 Settable for testing/mocking.
118 gsutil_api_class_map_factory: Creates map of cloud storage interfaces.
119 Settable for testing/mocking.
120 command_map: Map of command names to their implementations for
121 testing/mocking. If not set, the map is built dynamically.
123 self.bucket_storage_uri_class = bucket_storage_uri_class
124 self.gsutil_api_class_map_factory = gsutil_api_class_map_factory
125 if command_map:
126 self.command_map = command_map
127 else:
128 self.command_map = self._LoadCommandMap()
130 def _LoadCommandMap(self):
131 """Returns dict mapping each command_name to implementing class."""
132 # Import all gslib.commands submodules.
133 for _, module_name, _ in pkgutil.iter_modules(gslib.commands.__path__):
134 __import__('gslib.commands.%s' % module_name)
136 command_map = {}
137 # Only include Command subclasses in the dict.
138 for command in Command.__subclasses__():
139 command_map[command.command_spec.command_name] = command
140 for command_name_aliases in command.command_spec.command_name_aliases:
141 command_map[command_name_aliases] = command
142 return command_map
144 def _ConfigureCommandArgumentParserArguments(
145 self, parser, arguments, gsutil_api):
146 """Configures an argument parser with the given arguments.
148 Args:
149 parser: argparse parser object.
150 arguments: array of CommandArgument objects.
151 gsutil_api: gsutil Cloud API instance to use.
152 Raises:
153 RuntimeError: if argument is configured with unsupported completer
155 for command_argument in arguments:
156 action = parser.add_argument(
157 *command_argument.args, **command_argument.kwargs)
158 if command_argument.completer:
159 action.completer = MakeCompleter(command_argument.completer, gsutil_api)
161 def ConfigureCommandArgumentParsers(self, subparsers):
162 """Configures argparse arguments and argcomplete completers for commands.
164 Args:
165 subparsers: argparse object that can be used to add parsers for
166 subcommands (called just 'commands' in gsutil)
169 # This should match the support map for the "ls" command.
170 support_map = {
171 'gs': [ApiSelector.XML, ApiSelector.JSON],
172 's3': [ApiSelector.XML]
174 default_map = {
175 'gs': ApiSelector.JSON,
176 's3': ApiSelector.XML
178 gsutil_api_map = GsutilApiMapFactory.GetApiMap(
179 self.gsutil_api_class_map_factory, support_map, default_map)
181 logger = CreateGsutilLogger('tab_complete')
182 gsutil_api = CloudApiDelegator(
183 self.bucket_storage_uri_class, gsutil_api_map,
184 logger, debug=0)
186 for command in set(self.command_map.values()):
187 command_parser = subparsers.add_parser(
188 command.command_spec.command_name, add_help=False)
189 if isinstance(command.command_spec.argparse_arguments, dict):
190 subcommand_parsers = command_parser.add_subparsers()
191 subcommand_argument_dict = command.command_spec.argparse_arguments
192 for subcommand, arguments in subcommand_argument_dict.iteritems():
193 subcommand_parser = subcommand_parsers.add_parser(
194 subcommand, add_help=False)
195 self._ConfigureCommandArgumentParserArguments(
196 subcommand_parser, arguments, gsutil_api)
197 else:
198 self._ConfigureCommandArgumentParserArguments(
199 command_parser, command.command_spec.argparse_arguments, gsutil_api)
201 def RunNamedCommand(self, command_name, args=None, headers=None, debug=0,
202 parallel_operations=False, test_method=None,
203 skip_update_check=False, logging_filters=None,
204 do_shutdown=True):
205 """Runs the named command.
207 Used by gsutil main, commands built atop other commands, and tests.
209 Args:
210 command_name: The name of the command being run.
211 args: Command-line args (arg0 = actual arg, not command name ala bash).
212 headers: Dictionary containing optional HTTP headers to pass to boto.
213 debug: Debug level to pass in to boto connection (range 0..3).
214 parallel_operations: Should command operations be executed in parallel?
215 test_method: Optional general purpose method for testing purposes.
216 Application and semantics of this method will vary by
217 command and test type.
218 skip_update_check: Set to True to disable checking for gsutil updates.
219 logging_filters: Optional list of logging.Filters to apply to this
220 command's logger.
221 do_shutdown: Stop all parallelism framework workers iff this is True.
223 Raises:
224 CommandException: if errors encountered.
226 Returns:
227 Return value(s) from Command that was run.
229 if (not skip_update_check and
230 self.MaybeCheckForAndOfferSoftwareUpdate(command_name, debug)):
231 command_name = 'update'
232 args = ['-n']
234 if not args:
235 args = []
237 # Include api_version header in all commands.
238 api_version = boto.config.get_value('GSUtil', 'default_api_version', '1')
239 if not headers:
240 headers = {}
241 headers['x-goog-api-version'] = api_version
243 if command_name not in self.command_map:
244 close_matches = difflib.get_close_matches(
245 command_name, self.command_map.keys(), n=1)
246 if close_matches:
247 # Instead of suggesting a deprecated command alias, suggest the new
248 # name for that command.
249 translated_command_name = (
250 OLD_ALIAS_MAP.get(close_matches[0], close_matches)[0])
251 print >> sys.stderr, 'Did you mean this?'
252 print >> sys.stderr, '\t%s' % translated_command_name
253 elif command_name == 'update' and gslib.IS_PACKAGE_INSTALL:
254 sys.stderr.write(
255 'Update command is not supported for package installs; '
256 'please instead update using your package manager.')
258 raise CommandException('Invalid command "%s".' % command_name)
259 if '--help' in args:
260 new_args = [command_name]
261 original_command_class = self.command_map[command_name]
262 subcommands = original_command_class.help_spec.subcommand_help_text.keys()
263 for arg in args:
264 if arg in subcommands:
265 new_args.append(arg)
266 break # Take the first match and throw away the rest.
267 args = new_args
268 command_name = 'help'
270 args = HandleArgCoding(args)
272 command_class = self.command_map[command_name]
273 command_inst = command_class(
274 self, args, headers, debug, parallel_operations,
275 self.bucket_storage_uri_class, self.gsutil_api_class_map_factory,
276 test_method, logging_filters, command_alias_used=command_name)
277 return_code = command_inst.RunCommand()
279 if MultiprocessingIsAvailable()[0] and do_shutdown:
280 ShutDownGsutil()
281 if GetFailureCount() > 0:
282 return_code = 1
283 return return_code
285 def MaybeCheckForAndOfferSoftwareUpdate(self, command_name, debug):
286 """Checks the last time we checked for an update and offers one if needed.
288 Offer is made if the time since the last update check is longer
289 than the configured threshold offers the user to update gsutil.
291 Args:
292 command_name: The name of the command being run.
293 debug: Debug level to pass in to boto connection (range 0..3).
295 Returns:
296 True if the user decides to update.
298 # Don't try to interact with user if:
299 # - gsutil is not connected to a tty (e.g., if being run from cron);
300 # - user is running gsutil -q
301 # - user is running the config command (which could otherwise attempt to
302 # check for an update for a user running behind a proxy, who has not yet
303 # configured gsutil to go through the proxy; for such users we need the
304 # first connection attempt to be made by the gsutil config command).
305 # - user is running the version command (which gets run when using
306 # gsutil -D, which would prevent users with proxy config problems from
307 # sending us gsutil -D output).
308 # - user is running the update command (which could otherwise cause an
309 # additional note that an update is available when user is already trying
310 # to perform an update);
311 # - user specified gs_host (which could be a non-production different
312 # service instance, in which case credentials won't work for checking
313 # gsutil tarball).
314 # - user is using a Cloud SDK install (which should only be updated via
315 # gcloud components update)
316 logger = logging.getLogger()
317 gs_host = boto.config.get('Credentials', 'gs_host', None)
318 if (not IsRunningInteractively()
319 or command_name in ('config', 'update', 'ver', 'version')
320 or not logger.isEnabledFor(logging.INFO)
321 or gs_host
322 or os.environ.get('CLOUDSDK_WRAPPER') == '1'):
323 return False
325 software_update_check_period = boto.config.getint(
326 'GSUtil', 'software_update_check_period', 30)
327 # Setting software_update_check_period to 0 means periodic software
328 # update checking is disabled.
329 if software_update_check_period == 0:
330 return False
332 cur_ts = int(time.time())
333 if not os.path.isfile(LAST_CHECKED_FOR_GSUTIL_UPDATE_TIMESTAMP_FILE):
334 # Set last_checked_ts from date of VERSION file, so if the user installed
335 # an old copy of gsutil it will get noticed (and an update offered) the
336 # first time they try to run it.
337 last_checked_ts = GetGsutilVersionModifiedTime()
338 with open(LAST_CHECKED_FOR_GSUTIL_UPDATE_TIMESTAMP_FILE, 'w') as f:
339 f.write(str(last_checked_ts))
340 else:
341 try:
342 with open(LAST_CHECKED_FOR_GSUTIL_UPDATE_TIMESTAMP_FILE, 'r') as f:
343 last_checked_ts = int(f.readline())
344 except (TypeError, ValueError):
345 return False
347 if (cur_ts - last_checked_ts
348 > software_update_check_period * SECONDS_PER_DAY):
349 # Create a credential-less gsutil API to check for the public
350 # update tarball.
351 gsutil_api = GcsJsonApi(self.bucket_storage_uri_class, logger,
352 credentials=NoOpCredentials(), debug=debug)
354 cur_ver = LookUpGsutilVersion(gsutil_api, GSUTIL_PUB_TARBALL)
355 with open(LAST_CHECKED_FOR_GSUTIL_UPDATE_TIMESTAMP_FILE, 'w') as f:
356 f.write(str(cur_ts))
357 (g, m) = CompareVersions(cur_ver, gslib.VERSION)
358 if m:
359 print '\n'.join(textwrap.wrap(
360 'A newer version of gsutil (%s) is available than the version you '
361 'are running (%s). NOTE: This is a major new version, so it is '
362 'strongly recommended that you review the release note details at '
363 '%s before updating to this version, especially if you use gsutil '
364 'in scripts.' % (cur_ver, gslib.VERSION, RELEASE_NOTES_URL)))
365 if gslib.IS_PACKAGE_INSTALL:
366 return False
367 print
368 answer = raw_input('Would you like to update [y/N]? ')
369 return answer and answer.lower()[0] == 'y'
370 elif g:
371 print '\n'.join(textwrap.wrap(
372 'A newer version of gsutil (%s) is available than the version you '
373 'are running (%s). A detailed log of gsutil release changes is '
374 'available at %s if you would like to read them before updating.'
375 % (cur_ver, gslib.VERSION, RELEASE_NOTES_URL)))
376 if gslib.IS_PACKAGE_INSTALL:
377 return False
378 print
379 answer = raw_input('Would you like to update [Y/n]? ')
380 return not answer or answer.lower()[0] != 'n'
381 return False