#!/usr/bin/env python # -*- coding: utf-8 -*- # # # Copyright 2013 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """gcloud command line tool.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import time START_TIME = time.time() # pylint:disable=g-bad-import-order # pylint:disable=g-import-not-at-top, We want to get the start time first. import atexit import errno import os import sys from googlecloudsdk.calliope import base from googlecloudsdk.calliope import cli from googlecloudsdk.command_lib import crash_handling from googlecloudsdk.command_lib.util.apis import yaml_command_translator from googlecloudsdk.core import config from googlecloudsdk.core import log from googlecloudsdk.core import metrics from googlecloudsdk.core import properties from googlecloudsdk.core.credentials import creds_context_managers from googlecloudsdk.core.credentials import devshell as c_devshell from googlecloudsdk.core.survey import survey_check from googlecloudsdk.core.updater import local_state from googlecloudsdk.core.util import keyboard_interrupt from googlecloudsdk.core.util import platforms import surface # Disable stack traces when the command is interrupted. keyboard_interrupt.InstallHandler() if not config.Paths().sdk_root: # Don't do update checks if there is no install root. properties.VALUES.component_manager.disable_update_check.Set(True) def UpdateCheck(command_path, **unused_kwargs): from googlecloudsdk.core.updater import update_manager try: update_manager.UpdateManager.PerformUpdateCheck(command_path=command_path) # pylint:disable=broad-except, We never want this to escape, ever. Only # messages printed should reach the user. except Exception: log.debug('Failed to perform update check.', exc_info=True) def _ShouldCheckSurveyPrompt(command_path): """Decides if survey prompt should be checked.""" if properties.VALUES.survey.disable_prompts.GetBool(): return False # dev shell environment uses temporary folder for user config. That means # survey prompt cache gets cleaned each time user starts a new session, # which results in too frequent prompting. if c_devshell.IsDevshellEnvironment(): return False exempt_commands = [ 'gcloud.components.post-process', ] for exempt_command in exempt_commands: if command_path.startswith(exempt_command): return False return True def SurveyPromptCheck(command_path, **unused_kwargs): """Checks for in-tool survey prompt.""" if not _ShouldCheckSurveyPrompt(command_path): return try: survey_check.SurveyPrompter().Prompt() # pylint:disable=broad-except, We never want this to escape, ever. Only # messages printed should reach the user. except Exception: log.debug('Failed to check survey prompt.', exc_info=True) # pylint:enable=broad-except def CreateCLI(surfaces, translator=None): """Generates the gcloud CLI from 'surface' folder with extra surfaces. Args: surfaces: list(tuple(dot_path, dir_path)), extra commands or subsurfaces to add, where dot_path is calliope command path and dir_path path to command group or command. translator: yaml_command_translator.Translator, an alternative translator. Returns: calliope cli object. """ def VersionFunc(): generated_cli.Execute(['version']) def HandleKnownErrorFunc(): crash_handling.ReportError(is_crash=False) pkg_root = os.path.dirname(os.path.dirname(surface.__file__)) loader = cli.CLILoader( name='gcloud', command_root_directory=os.path.join(pkg_root, 'surface'), allow_non_existing_modules=True, version_func=VersionFunc, known_error_handler=HandleKnownErrorFunc, yaml_command_translator=(translator or yaml_command_translator.Translator()), ) loader.AddReleaseTrack( base.ReleaseTrack.ALPHA, os.path.join(pkg_root, 'surface', 'alpha'), component='alpha') loader.AddReleaseTrack( base.ReleaseTrack.BETA, os.path.join(pkg_root, 'surface', 'beta'), component='beta') loader.AddReleaseTrack( base.ReleaseTrack.PREVIEW, os.path.join(pkg_root, 'surface', 'preview'), component='preview') for dot_path, dir_path in surfaces: loader.AddModule(dot_path, dir_path, component=None) # Clone 'container/hub' surface into 'container/fleet' # for backward compatibility. loader.AddModule( 'container.hub', os.path.join(pkg_root, 'surface', 'container', 'fleet')) # Make 'bigtable.tables' an alias for 'bigtable.instances.tables' to be # consistent with other bigtable commands while avoiding a breaking change. loader.AddModule( 'bigtable.tables', os.path.join(pkg_root, 'surface', 'bigtable', 'instances', 'tables'), ) # TODO(b/399010656): Clone 'migration' surface into 'compute/migration'. # TODO(b/433619731): Remove cloned migration commands after a suitable # deprecation period. loader.AddModule( 'compute.migration', os.path.join(pkg_root, 'surface', 'migration', 'vms') ) # Check for updates on shutdown but not for any of the updater commands. # Skip update checks for 'gcloud version' command as it does that manually. exclude_commands = r'gcloud\.components\..*|gcloud\.version' loader.RegisterPostRunHook(UpdateCheck, exclude_commands=exclude_commands) loader.RegisterPostRunHook(SurveyPromptCheck) generated_cli = loader.Generate() return generated_cli @crash_handling.CrashManager def main(gcloud_cli=None, credential_providers=None): atexit.register(metrics.Shutdown) if not platforms.PythonVersion().IsCompatible(): sys.exit(1) metrics.Started(START_TIME) metrics.Executions( 'gcloud', local_state.InstallationState.VersionForInstalledComponent('core')) if gcloud_cli is None: gcloud_cli = CreateCLI([]) with creds_context_managers.CredentialProvidersManager(credential_providers): try: gcloud_cli.Execute() # Flush stdout so that if we've received a SIGPIPE we handle the broken # pipe within this try block, instead of potentially during interpreter # shutdown. sys.stdout.flush() except IOError as err: # We want to ignore EPIPE IOErrors (as of Python 3.3 these can be caught # specifically with BrokenPipeError, but we do it this way for Python 2 # compatibility). # # By default, Python ignores SIGPIPE (see # http://utcc.utoronto.ca/~cks/space/blog/python/SignalExceptionSurprise # ). # This means that attempting to write any output to a closed pipe (e.g. # in the case of output piped to `head` or `grep -q`) will result in an # IOError, which gets reported as a gcloud crash. We don't want this # behavior, so we ignore EPIPE (it's not a real error; it's a normal # thing to occur). # # Before, we restored the SIGPIPE signal handler, but that caused issues # with scripts/programs that wrapped gcloud. if err.errno == errno.EPIPE: # At this point we've caught the broken pipe, but since Python flushes # standard streams on exit, it's still possible for a broken pipe # error to happen during interpreter shutdown. The interpreter will # catch this but in Python 3 it still prints a warning to stderr # saying that the exception was ignored # (see https://bugs.python.org/issue11380): # # Exception ignored in: <_io.TextIOWrapper name='' mode='w' # encoding='UTF-8'> # BrokenPipeError: [Errno 32] Broken pipe # # To prevent this from happening, we redirect any remaining output to # devnull as recommended here: # https://docs.python.org/3/library/signal.html#note-on-sigpipe. devnull = os.open(os.devnull, os.O_WRONLY) os.dup2(devnull, sys.stdout.fileno()) else: raise if __name__ == '__main__': try: main() except KeyboardInterrupt: keyboard_interrupt.HandleInterrupt()