1# -*- coding: utf-8 -*- # 2# Copyright 2013 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""argparse Actions for use with calliope. 17""" 18 19from __future__ import absolute_import 20from __future__ import division 21from __future__ import unicode_literals 22 23import argparse 24import io 25import os 26import sys 27 28from googlecloudsdk.calliope import base 29from googlecloudsdk.calliope import markdown 30from googlecloudsdk.calliope import parser_errors 31from googlecloudsdk.core import log 32from googlecloudsdk.core import metrics 33from googlecloudsdk.core import properties 34from googlecloudsdk.core.console import console_io 35from googlecloudsdk.core.document_renderers import render_document 36import six 37 38 39class _AdditionalHelp(object): 40 """Simple class for passing additional help messages to Actions.""" 41 42 def __init__(self, label, message): 43 self.label = label 44 self.message = message 45 46 47def GetArgparseBuiltInAction(action): 48 """Get an argparse.Action from a string. 49 50 This function takes one of the supplied argparse.Action strings (see below) 51 and returns the corresponding argparse.Action class. 52 53 This "work around" is (e.g. hack) is necessary due to the fact these required 54 action mappings are only exposed through subclasses of 55 argparse._ActionsContainer as opposed to a static function or global variable. 56 57 Args: 58 action: string, one of the following supplied argparse.Action names: 59 'store', 'store_const', 'store_false', 'append', 'append_const', 'count', 60 'version', 'parsers'. 61 62 Returns: 63 argparse.Action, the action class to use. 64 65 Raises: 66 ValueError: For unknown action string. 67 """ 68 # pylint:disable=protected-access 69 # Disabling lint check to access argparse._ActionsContainer 70 dummy_actions_container = argparse._ActionsContainer(description=None, 71 prefix_chars=None, 72 argument_default=None, 73 conflict_handler='error') 74 75 action_cls = dummy_actions_container._registry_get('action', action) 76 77 if action_cls is None: 78 raise ValueError('unknown action "{0}"'.format(action)) 79 80 return action_cls 81 # pylint:disable=protected-access 82 83 84def FunctionExitAction(func): 85 """Get an argparse.Action that runs the provided function, and exits. 86 87 Args: 88 func: func, the function to execute. 89 90 Returns: 91 argparse.Action, the action to use. 92 """ 93 94 class Action(argparse.Action): 95 """The action created for FunctionExitAction.""" 96 97 def __init__(self, **kwargs): 98 kwargs['nargs'] = 0 99 super(Action, self).__init__(**kwargs) 100 101 def __call__(self, parser, namespace, values, option_string=None): 102 base.LogCommand(parser.prog, namespace) 103 metrics.Loaded() 104 func() 105 sys.exit(0) 106 107 return Action 108 109 110def StoreProperty(prop): 111 """Get an argparse action that stores a value in a property. 112 113 Also stores the value in the namespace object, like the default action. The 114 value is stored in the invocation stack, rather than persisted permanently. 115 116 Args: 117 prop: properties._Property, The property that should get the invocation 118 value. 119 120 Returns: 121 argparse.Action, An argparse action that routes the value correctly. 122 """ 123 124 class Action(argparse.Action): 125 """The action created for StoreProperty.""" 126 127 # store_property is referenced in calliope.parser_arguments.add_argument 128 store_property = (prop, None, None) 129 130 def __init__(self, *args, **kwargs): 131 super(Action, self).__init__(*args, **kwargs) 132 option_strings = kwargs.get('option_strings') 133 if option_strings: 134 option_string = option_strings[0] 135 else: 136 option_string = None 137 properties.VALUES.SetInvocationValue(prop, None, option_string) 138 139 if '_ARGCOMPLETE' in os.environ: 140 self._orig_class = argparse._StoreAction # pylint:disable=protected-access 141 142 def __call__(self, parser, namespace, values, option_string=None): 143 properties.VALUES.SetInvocationValue(prop, values, option_string) 144 setattr(namespace, self.dest, values) 145 146 return Action 147 148 149def StoreBooleanProperty(prop): 150 """Get an argparse action that stores a value in a Boolean property. 151 152 Handles auto-generated --no-* inverted flags by inverting the value. 153 154 Also stores the value in the namespace object, like the default action. The 155 value is stored in the invocation stack, rather than persisted permanently. 156 157 Args: 158 prop: properties._Property, The property that should get the invocation 159 value. 160 161 Returns: 162 argparse.Action, An argparse action that routes the value correctly. 163 """ 164 165 class Action(argparse.Action): 166 """The action created for StoreBooleanProperty.""" 167 168 # store_property is referenced in calliope.parser_arguments.add_argument 169 store_property = (prop, 'bool', None) 170 171 def __init__(self, *args, **kwargs): 172 kwargs = dict(kwargs) 173 # Bool flags don't take any args. There is one legacy one that needs to 174 # so only do this if the flag doesn't specifically register nargs. 175 if 'nargs' not in kwargs: 176 kwargs['nargs'] = 0 177 178 option_strings = kwargs.get('option_strings') 179 if option_strings: 180 option_string = option_strings[0] 181 else: 182 option_string = None 183 if option_string and option_string.startswith('--no-'): 184 self._inverted = True 185 kwargs['nargs'] = 0 186 kwargs['const'] = None 187 kwargs['choices'] = None 188 else: 189 self._inverted = False 190 super(Action, self).__init__(*args, **kwargs) 191 properties.VALUES.SetInvocationValue(prop, None, option_string) 192 193 if '_ARGCOMPLETE' in os.environ: 194 self._orig_class = argparse._StoreAction # pylint:disable=protected-access 195 196 def __call__(self, parser, namespace, values, option_string=None): 197 if self._inverted: 198 if values in ('true', []): 199 values = 'false' 200 else: 201 values = 'false' 202 elif values == []: # pylint: disable=g-explicit-bool-comparison, need exact [] equality test 203 values = 'true' 204 properties.VALUES.SetInvocationValue(prop, values, option_string) 205 setattr(namespace, self.dest, values) 206 207 return Action 208 209 210def StoreConstProperty(prop, const): 211 """Get an argparse action that stores a constant in a property. 212 213 Also stores the constant in the namespace object, like the store_true action. 214 The const is stored in the invocation stack, rather than persisted 215 permanently. 216 217 Args: 218 prop: properties._Property, The property that should get the invocation 219 value. 220 const: str, The constant that should be stored in the property. 221 222 Returns: 223 argparse.Action, An argparse action that routes the value correctly. 224 """ 225 226 class Action(argparse.Action): 227 """The action created for StoreConstProperty.""" 228 229 # store_property is referenced in calliope.parser_arguments.add_argument 230 store_property = (prop, 'value', const) 231 232 def __init__(self, *args, **kwargs): 233 kwargs = dict(kwargs) 234 kwargs['nargs'] = 0 235 super(Action, self).__init__(*args, **kwargs) 236 237 if '_ARGCOMPLETE' in os.environ: 238 self._orig_class = argparse._StoreConstAction # pylint:disable=protected-access 239 240 def __call__(self, parser, namespace, values, option_string=None): 241 properties.VALUES.SetInvocationValue(prop, const, option_string) 242 setattr(namespace, self.dest, const) 243 244 return Action 245 246 247# pylint:disable=pointless-string-statement 248""" Some example short help outputs follow. 249 250$ gcloud -h 251usage: gcloud [optional flags] <group | command> 252 group is one of auth | components | config | dns | sql 253 command is one of init | interactive | su | version 254 255Google Cloud Platform CLI/API. 256 257optional flags: 258 -h, --help Print this help message and exit. 259 --project PROJECT Google Cloud Platform project to use for this 260 invocation. 261 --quiet, -q Disable all interactive prompts when running gcloud 262 commands. If input is required, defaults will be used, 263 or an error will be raised. 264 265groups: 266 auth Manage oauth2 credentials for the Google Cloud SDK. 267 components Install, update, or remove the tools in the Google 268 Cloud SDK. 269 config View and edit Google Cloud SDK properties. 270 dns Manage Cloud DNS. 271 sql Manage Cloud SQL databases. 272 273commands: 274 init Initialize a gcloud workspace in the current directory. 275 interactive Use this tool in an interactive python shell. 276 su Switch the user account. 277 version Print version information for Cloud SDK components. 278 279 280 281$ gcloud auth -h 282usage: gcloud auth [optional flags] <command> 283 command is one of activate_git_p2d | activate_refresh_token | 284 activate_service_account | list | login | revoke 285 286Manage oauth2 credentials for the Google Cloud SDK. 287 288optional flags: 289 -h, --help Print this help message and exit. 290 291commands: 292 activate_git_p2d Activate an account for git push-to-deploy. 293 activate_refresh_token 294 Get credentials via an existing refresh token. 295 activate_service_account 296 Get credentials via the private key for a service 297 account. 298 list List the accounts for known credentials. 299 login Get credentials via Google's oauth2 web flow. 300 revoke Revoke authorization for credentials. 301 302 303 304$ gcloud sql instances create -h 305usage: gcloud sql instances create 306 [optional flags] INSTANCE 307 308Creates a new Cloud SQL instance. 309 310optional flags: 311 -h, --help Print this help message and exit. 312 --authorized-networks AUTHORIZED_NETWORKS 313 The list of external networks that are allowed to 314 connect to the instance. Specified in CIDR notation, 315 also known as 'slash' notation (e.g. 192.168.100.0/24). 316 --authorized-gae-apps AUTHORIZED_GAE_APPS 317 List of App Engine app ids that can access this 318 instance. 319 --activation-policy ACTIVATION_POLICY; default="ON_DEMAND" 320 The activation policy for this instance. This specifies 321 when the instance should be activated and is applicable 322 only when the instance state is RUNNABLE. Defaults to 323 ON_DEMAND. 324 --follow-gae-app FOLLOW_GAE_APP 325 The App Engine app this instance should follow. It must 326 be in the same region as the instance. 327 --backup-start-time BACKUP_START_TIME 328 Start time for the daily backup configuration in UTC 329 timezone,in the 24 hour format - HH:MM. 330 --gce-zone GCE_ZONE The preferred Compute Engine zone (e.g. us-central1-a, 331 us-central1-b, etc.). 332 --pricing-plan PRICING_PLAN, -p PRICING_PLAN; default="PER_USE" 333 The pricing plan for this instance. Defaults to 334 PER_USE. 335 --region REGION; default="us-east1" 336 The geographical region. Can be us-east1 or europe- 337 west1. Defaults to us-east1. 338 --replication REPLICATION; default="SYNCHRONOUS" 339 The type of replication this instance uses. Defaults to 340 SYNCHRONOUS. 341 --tier TIER, -t TIER; default="D0" 342 The tier of service for this instance, for example D0, 343 D1. Defaults to D0. 344 --assign-ip Specified if the instance must be assigned an IP 345 address. 346 --enable-bin-log Specified if binary log must be enabled. If backup 347 configuration is disabled, binary log must be disabled 348 as well. 349 --no-backup Specified if daily backup must be disabled. 350 351positional arguments: 352 INSTANCE Cloud SQL instance ID. 353 354 355""" 356 357# pylint:disable=pointless-string-statement 358""" 359$ gcloud auth activate-service-account -h 360usage: gcloud auth activate-service-account 361 --key-file=KEY_FILE [optional flags] ACCOUNT 362 363Get credentials for a service account, using a .p12 file for the private key. If 364--project is set, set the default project. 365 366required flags: 367 --key-file KEY_FILE Path to the service accounts private key. 368 369optional flags: 370 -h, --help Print this help message and exit. 371 --password-file PASSWORD_FILE 372 Path to a file containing the password for the service 373 account private key. 374 --prompt-for-password Prompt for the password for the service account private 375 key. 376 377positional arguments: 378 ACCOUNT The email for the service account. 379 380""" 381 382 383def ShortHelpAction(command): 384 """Get an argparse.Action that prints a short help. 385 386 Args: 387 command: calliope._CommandCommon, The command object that we're helping. 388 389 Returns: 390 argparse.Action, the action to use. 391 """ 392 def Func(): 393 metrics.Help(command.dotted_name, '-h') 394 log.out.write(command.GetUsage()) 395 return FunctionExitAction(Func) 396 397 398def RenderDocumentAction(command, default_style=None): 399 """Get an argparse.Action that renders a help document from markdown. 400 401 Args: 402 command: calliope._CommandCommon, The command object that we're helping. 403 default_style: str, The default style if not specified in flag value. 404 405 Returns: 406 argparse.Action, The action to use. 407 """ 408 409 class Action(argparse.Action): 410 """The action created for RenderDocumentAction.""" 411 412 def __init__(self, **kwargs): 413 if default_style: 414 kwargs['nargs'] = 0 415 super(Action, self).__init__(**kwargs) 416 417 def __call__(self, parser, namespace, values, option_string=None): 418 """Render a help document according to the style in values. 419 420 Args: 421 parser: The ArgParse object. 422 namespace: The ArgParse namespace. 423 values: The --document flag ArgDict() value: 424 style=STYLE 425 The output style. Must be specified. 426 title=DOCUMENT TITLE 427 The document title. 428 notes=SENTENCES 429 Inserts SENTENCES into the document NOTES section. 430 option_string: The ArgParse flag string. 431 432 Raises: 433 parser_errors.ArgumentError: For unknown flag value attribute name. 434 """ 435 base.LogCommand(parser.prog, namespace) 436 if default_style: 437 # --help 438 metrics.Loaded() 439 style = default_style 440 notes = None 441 title = None 442 443 for attributes in values: 444 for name, value in six.iteritems(attributes): 445 if name == 'notes': 446 notes = value 447 elif name == 'style': 448 style = value 449 elif name == 'title': 450 title = value 451 else: 452 raise parser_errors.ArgumentError( 453 'Unknown document attribute [{0}]'.format(name)) 454 455 if title is None: 456 title = command.dotted_name 457 458 metrics.Help(command.dotted_name, style) 459 # '--help' is set by the --help flag, the others by gcloud <style> ... . 460 if style in ('--help', 'help', 'topic'): 461 style = 'text' 462 md = io.StringIO(markdown.Markdown(command)) 463 out = (io.StringIO() if console_io.IsInteractive(output=True) 464 else None) 465 466 if style == 'linter': 467 meta_data = GetCommandMetaData(command) 468 else: 469 meta_data = None 470 render_document.RenderDocument(style, md, out=out or log.out, notes=notes, 471 title=title, command_metadata=meta_data) 472 metrics.Ran() 473 if out: 474 console_io.More(out.getvalue()) 475 476 sys.exit(0) 477 478 return Action 479 480 481def GetCommandMetaData(command): 482 command_metadata = render_document.CommandMetaData() 483 for arg in command.GetAllAvailableFlags(): 484 for arg_name in arg.option_strings: 485 command_metadata.flags.append(arg_name) 486 if isinstance(arg, argparse._StoreConstAction): 487 command_metadata.bool_flags.append(arg_name) 488 command_metadata.is_group = command.is_group 489 return command_metadata 490 491 492def _PreActionHook(action, func, additional_help=None): 493 """Allows an function hook to be injected before an Action executes. 494 495 Wraps an Action in another action that can execute an arbitrary function on 496 the argument value before passing invocation to underlying action. 497 This is useful for: 498 - Chaining actions together at runtime. 499 - Adding additional pre-processing or logging to an argument/flag 500 - Adding instrumentation to runtime execution of an flag without changing the 501 underlying intended behavior of the flag itself 502 503 Args: 504 action: action class to be wrapped. Either a subclass of argparse.Action 505 or a string representing one of the built in arg_parse action types. 506 If None, argparse._StoreAction type is used as default. 507 func: callable, function to be executed before invoking the __call__ method 508 of the wrapped action. Takes value from command line. 509 additional_help: _AdditionalHelp, Additional help (label, message) to be 510 added to action help 511 512 Returns: 513 argparse.Action, wrapper action to use. 514 515 Raises: 516 TypeError: If action or func are invalid types. 517 """ 518 if not callable(func): 519 raise TypeError('func should be a callable of the form func(value)') 520 521 if not isinstance(action, six.string_types) and not issubclass( 522 action, argparse.Action): 523 raise TypeError(('action should be either a subclass of argparse.Action ' 524 'or a string representing one of the default argparse ' 525 'Action Types')) 526 527 class Action(argparse.Action): 528 """Action Wrapper Class.""" 529 wrapped_action = action 530 531 @classmethod 532 def SetWrappedAction(cls, action): 533 # This looks potentially scary, but is OK because the Action class 534 # is enclosed within the _PreActionHook function. 535 cls.wrapped_action = action 536 537 def _GetActionClass(self): 538 if isinstance(self.wrapped_action, six.string_types): 539 action_cls = GetArgparseBuiltInAction(self.wrapped_action) 540 else: 541 action_cls = self.wrapped_action 542 return action_cls 543 544 def __init__(self, *args, **kwargs): 545 if additional_help: 546 original_help = kwargs.get('help', '').rstrip() 547 kwargs['help'] = '{0} {1}\n+\n{2}'.format( 548 additional_help.label, 549 original_help, 550 additional_help.message) 551 552 self._wrapped_action = self._GetActionClass()(*args, **kwargs) 553 self.func = func 554 # These parameters are necessary to ensure that the wrapper action 555 # behaves the same as the action it is wrapping. These based off of 556 # analysis of the constructor params (and their defaults) or the built in 557 # argparse Action classes. This could change if argparse internals are 558 # updated, but that would probably also affect much more than this. 559 kwargs['nargs'] = self._wrapped_action.nargs 560 kwargs['const'] = self._wrapped_action.const 561 kwargs['choices'] = self._wrapped_action.choices 562 kwargs['option_strings'] = self._wrapped_action.option_strings 563 super(Action, self).__init__(*args, **kwargs) 564 565 def __call__(self, parser, namespace, value, option_string=None): 566 # Fix for _Append and _AppendConst to only run self.func once. 567 flag_value = getattr(namespace, self.dest, None) 568 if isinstance(flag_value, list): 569 if len(flag_value) < 1: 570 self.func(value) 571 elif not value: 572 # For boolean flags use implied value, not explicit value 573 self.func(self._wrapped_action.const) 574 else: 575 self.func(value) 576 577 self._wrapped_action(parser, namespace, value, option_string) 578 579 return Action 580 581 582def DeprecationAction(flag_name, 583 show_message=lambda _: True, 584 show_add_help=lambda _: True, 585 warn='Flag {flag_name} is deprecated.', 586 error='Flag {flag_name} has been removed.', 587 removed=False, 588 action=None): 589 """Prints a warning or error message for a flag that is being deprecated. 590 591 Uses a _PreActionHook to wrap any existing Action on the flag and 592 also adds deprecation messaging to flag help. 593 594 Args: 595 flag_name: string, name of flag to be deprecated 596 show_message: callable, boolean function that takes the argument value 597 as input, validates it against some criteria and returns a boolean. 598 If true deprecation message is shown at runtime. Deprecation message 599 will always be appended to flag help. 600 show_add_help: boolean, whether to show additional help in help text. 601 warn: string, warning message, 'flag_name' template will be replaced with 602 value of flag_name parameter 603 error: string, error message, 'flag_name' template will be replaced with 604 value of flag_name parameter 605 removed: boolean, if True warning message will be printed when show_message 606 fails, if False error message will be printed 607 action: argparse.Action, action to be wrapped by this action 608 609 Returns: 610 argparse.Action, deprecation action to use. 611 """ 612 if removed: 613 add_help = _AdditionalHelp('(REMOVED)', error.format(flag_name=flag_name)) 614 else: 615 add_help = _AdditionalHelp('(DEPRECATED)', warn.format(flag_name=flag_name)) 616 617 if not action: # Default Action 618 action = 'store' 619 620 def DeprecationFunc(value): 621 if show_message(value): 622 if removed: 623 raise parser_errors.ArgumentError(add_help.message) 624 else: 625 log.warning(add_help.message) 626 627 if show_add_help: 628 return _PreActionHook(action, DeprecationFunc, add_help) 629 630 return _PreActionHook(action, DeprecationFunc, None) 631