1# -*- coding: utf-8 -*- # 2# Copyright 2020 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"""Base classes for calliope commands and groups. 16 17""" 18 19from __future__ import absolute_import 20from __future__ import division 21from __future__ import unicode_literals 22 23import abc 24import collections 25from functools import wraps # pylint:disable=g-importing-member 26import itertools 27import re 28import sys 29 30from googlecloudsdk.calliope import arg_parsers 31from googlecloudsdk.calliope import display 32from googlecloudsdk.core import exceptions 33from googlecloudsdk.core import log 34from googlecloudsdk.core import properties 35from googlecloudsdk.core.resource import resource_printer 36 37import six 38 39# Category constants 40AI_AND_MACHINE_LEARNING_CATEGORY = 'AI and Machine Learning' 41API_PLATFORM_AND_ECOSYSTEMS_CATEGORY = 'API Platform and Ecosystems' 42ANTHOS_CLI_CATEGORY = 'Anthos CLI' 43COMPUTE_CATEGORY = 'Compute' 44DATA_ANALYTICS_CATEGORY = 'Data Analytics' 45DATABASES_CATEGORY = 'Databases' 46IDENTITY_AND_SECURITY_CATEGORY = 'Identity and Security' 47INTERNET_OF_THINGS_CATEGORY = 'Internet of Things' 48MANAGEMENT_TOOLS_CATEGORY = 'Management Tools' 49MOBILE_CATEGORY = 'Mobile' 50NETWORKING_CATEGORY = 'Networking' 51SDK_TOOLS_CATEGORY = 'SDK Tools' 52DISKS_CATEGORY = 'Disks' 53INFO_CATEGORY = 'Info' 54INSTANCES_CATEGORY = 'Instances' 55LOAD_BALANCING_CATEGORY = 'Load Balancing' 56TOOLS_CATEGORY = 'Tools' 57STORAGE_CATEGORY = 'Storage' 58BILLING_CATEGORY = 'Billing' 59SECURITY_CATEGORY = 'Security' 60IDENTITY_CATEGORY = 'Identity' 61BIG_DATA_CATEGORY = 'Big Data' 62CI_CD_CATEGORY = 'CI/CD' 63MONITORING_CATEGORY = 'Monitoring' 64SOLUTIONS_CATEGORY = 'Solutions' 65SERVERLESS_CATEGORY = 'Serverless' 66UNCATEGORIZED_CATEGORY = 'Other' 67IDENTITY_CATEGORY = 'Identity' 68COMMERCE_CATEGORY = 'Commerce' 69DECLARATIVE_CONFIGURATION_CATEGORY = 'Declarative Configuration' 70 71 72# Common markdown. 73MARKDOWN_BOLD = '*' 74MARKDOWN_ITALIC = '_' 75MARKDOWN_CODE = '`' 76 77 78class DeprecationException(exceptions.Error): 79 """An exception for when a command or group has been deprecated.""" 80 81 82class ReleaseTrack(object): 83 """An enum representing the release track of a command or command group. 84 85 The release track controls where a command appears. The default of GA means 86 it will show up under gcloud. If you enable a command or group for the alpha, 87 beta, or preview tracks, those commands will be duplicated under those groups 88 as well. 89 """ 90 91 class _TRACK(object): 92 """An enum representing the release track of a command or command group.""" 93 94 # pylint: disable=redefined-builtin 95 def __init__(self, id, prefix, help_tag, help_note): 96 self.id = id 97 self.prefix = prefix 98 self.help_tag = help_tag 99 self.help_note = help_note 100 101 def __str__(self): 102 return self.id 103 104 def __eq__(self, other): 105 return self.id == other.id 106 107 def __hash__(self): 108 return hash(self.id) 109 110 GA = _TRACK('GA', None, None, None) 111 BETA = _TRACK( 112 'BETA', 'beta', 113 '{0}(BETA){0} '.format(MARKDOWN_BOLD), 114 'This command is currently in BETA and may change without notice.') 115 ALPHA = _TRACK( 116 'ALPHA', 'alpha', 117 '{0}(ALPHA){0} '.format(MARKDOWN_BOLD), 118 'This command is currently in ALPHA and may change without notice. ' 119 'If this command fails with API permission errors despite specifying ' 120 'the right project, you may be trying to access an API with ' 121 'an invitation-only early access allowlist.') 122 _ALL = [GA, BETA, ALPHA] 123 124 @staticmethod 125 def AllValues(): 126 """Gets all possible enum values. 127 128 Returns: 129 list, All the enum values. 130 """ 131 return list(ReleaseTrack._ALL) 132 133 @staticmethod 134 def FromPrefix(prefix): 135 """Gets a ReleaseTrack from the given release track prefix. 136 137 Args: 138 prefix: str, The prefix string that might be a release track name. 139 140 Returns: 141 ReleaseTrack, The corresponding object or None if the prefix was not a 142 valid release track. 143 """ 144 for track in ReleaseTrack._ALL: 145 if track.prefix == prefix: 146 return track 147 return None 148 149 @staticmethod 150 def FromId(id): # pylint: disable=redefined-builtin 151 """Gets a ReleaseTrack from the given release track prefix. 152 153 Args: 154 id: str, The id string that must be a release track name. 155 156 Raises: 157 ValueError: For unknown release track ids. 158 159 Returns: 160 ReleaseTrack, The corresponding object. 161 """ 162 for track in ReleaseTrack._ALL: 163 if track.id == id: 164 return track 165 raise ValueError('Unknown release track id [{}].'.format(id)) 166 167 168class Action(six.with_metaclass(abc.ABCMeta, object)): 169 """A class that allows you to save an Action configuration for reuse.""" 170 171 def __init__(self, *args, **kwargs): 172 """Creates the Action. 173 174 Args: 175 *args: The positional args to parser.add_argument. 176 **kwargs: The keyword args to parser.add_argument. 177 """ 178 self.args = args 179 self.kwargs = kwargs 180 181 @property 182 def name(self): 183 return self.args[0] 184 185 @abc.abstractmethod 186 def AddToParser(self, parser): 187 """Adds this Action to the given parser. 188 189 Args: 190 parser: The argparse parser. 191 192 Returns: 193 The result of adding the Action to the parser. 194 """ 195 pass 196 197 def RemoveFromParser(self, parser): 198 """Removes this Action from the given parser. 199 200 Args: 201 parser: The argparse parser. 202 """ 203 pass 204 205 def SetDefault(self, parser, default): 206 """Sets the default value for this Action in the given parser. 207 208 Args: 209 parser: The argparse parser. 210 default: The default value. 211 """ 212 pass 213 214 215class ArgumentGroup(Action): 216 """A class that allows you to save an argument group configuration for reuse. 217 """ 218 219 def __init__(self, *args, **kwargs): 220 super(ArgumentGroup, self).__init__(*args, **kwargs) 221 self.arguments = [] 222 223 def AddArgument(self, arg): 224 self.arguments.append(arg) 225 226 def AddToParser(self, parser): 227 """Adds this argument group to the given parser. 228 229 Args: 230 parser: The argparse parser. 231 232 Returns: 233 The result of parser.add_argument(). 234 """ 235 group = self._CreateGroup(parser) 236 for arg in self.arguments: 237 arg.AddToParser(group) 238 return group 239 240 def _CreateGroup(self, parser): 241 return parser.add_group(*self.args, **self.kwargs) 242 243 244class Argument(Action): 245 """A class that allows you to save an argument configuration for reuse.""" 246 247 def __GetFlag(self, parser): 248 """Returns the flag object in parser.""" 249 for flag in itertools.chain(parser.flag_args, parser.ancestor_flag_args): 250 if self.name in flag.option_strings: 251 return flag 252 return None 253 254 def AddToParser(self, parser): 255 """Adds this argument to the given parser. 256 257 Args: 258 parser: The argparse parser. 259 260 Returns: 261 The result of parser.add_argument(). 262 """ 263 return parser.add_argument(*self.args, **self.kwargs) 264 265 def RemoveFromParser(self, parser): 266 """Removes this flag from the given parser. 267 268 Args: 269 parser: The argparse parser. 270 """ 271 flag = self.__GetFlag(parser) 272 if flag: 273 # Remove the flag and its inverse, if it exists, from its container. 274 name = flag.option_strings[0] 275 conflicts = [(name, flag)] 276 no_name = '--no-' + name[2:] 277 for no_flag in itertools.chain(parser.flag_args, 278 parser.ancestor_flag_args): 279 if no_name in no_flag.option_strings: 280 conflicts.append((no_name, no_flag)) 281 # pylint: disable=protected-access, argparse, why can't we be friends 282 flag.container._handle_conflict_resolve(flag, conflicts) 283 # Remove the conflict flags from the calliope argument interceptor. 284 for _, flag in conflicts: 285 parser.defaults.pop(flag.dest, None) 286 if flag.dest in parser.dests: 287 parser.dests.remove(flag.dest) 288 if flag in parser.flag_args: 289 parser.flag_args.remove(flag) 290 if flag in parser.arguments: 291 parser.arguments.remove(flag) 292 293 def SetDefault(self, parser, default): 294 """Sets the default value for this flag in the given parser. 295 296 Args: 297 parser: The argparse parser. 298 default: The default flag value. 299 """ 300 flag = self.__GetFlag(parser) 301 if flag: 302 kwargs = {flag.dest: default} 303 parser.set_defaults(**kwargs) 304 305 # Update the flag's help text. 306 original_help = flag.help 307 match = re.search(r'(.*The default is ).*?(\.([ \t\n].*))', 308 original_help, re.DOTALL) 309 if match: 310 new_help = '{}*{}*{}'.format(match.group(1), default, match.group(2)) 311 else: 312 new_help = original_help + ' The default is *{}*.'.format(default) 313 flag.help = new_help 314 315# Common flag definitions for consistency. 316 317# Common flag categories. 318 319COMMONLY_USED_FLAGS = 'COMMONLY USED' 320 321FLAGS_FILE_FLAG = Argument( 322 '--flags-file', 323 metavar='YAML_FILE', 324 default=None, 325 category=COMMONLY_USED_FLAGS, 326 help="""\ 327 A YAML or JSON file that specifies a *--flag*:*value* dictionary. 328 Useful for specifying complex flag values with special characters 329 that work with any command interpreter. Additionally, each 330 *--flags-file* arg is replaced by its constituent flags. See 331 $ gcloud topic flags-file for more information.""") 332 333FLATTEN_FLAG = Argument( 334 '--flatten', 335 metavar='KEY', 336 default=None, 337 type=arg_parsers.ArgList(), 338 category=COMMONLY_USED_FLAGS, 339 help="""\ 340 Flatten _name_[] output resource slices in _KEY_ into separate records 341 for each item in each slice. Multiple keys and slices may be specified. 342 This also flattens keys for *--format* and *--filter*. For example, 343 *--flatten=abc.def* flattens *abc.def[].ghi* references to 344 *abc.def.ghi*. A resource record containing *abc.def[]* with N elements 345 will expand to N records in the flattened output. This flag interacts 346 with other flags that are applied in this order: *--flatten*, 347 *--sort-by*, *--filter*, *--limit*.""") 348 349FORMAT_FLAG = Argument( 350 '--format', 351 default=None, 352 category=COMMONLY_USED_FLAGS, 353 help="""\ 354 Set the format for printing command output resources. The default is a 355 command-specific human-friendly output format. The supported formats 356 are: `{0}`. For more details run $ gcloud topic formats.""".format( 357 '`, `'.join(resource_printer.SupportedFormats()))) 358 359LIST_COMMAND_FLAGS = 'LIST COMMAND' 360 361ASYNC_FLAG = Argument( 362 '--async', 363 action='store_true', 364 dest='async_', 365 help="""\ 366 Return immediately, without waiting for the operation in progress to 367 complete.""") 368 369FILTER_FLAG = Argument( 370 '--filter', 371 metavar='EXPRESSION', 372 require_coverage_in_tests=False, 373 category=LIST_COMMAND_FLAGS, 374 help="""\ 375 Apply a Boolean filter _EXPRESSION_ to each resource item to be listed. 376 If the expression evaluates `True`, then that item is listed. For more 377 details and examples of filter expressions, run $ gcloud topic filters. This 378 flag interacts with other flags that are applied in this order: *--flatten*, 379 *--sort-by*, *--filter*, *--limit*.""") 380 381LIMIT_FLAG = Argument( 382 '--limit', 383 type=arg_parsers.BoundedInt(1, sys.maxsize, unlimited=True), 384 require_coverage_in_tests=False, 385 category=LIST_COMMAND_FLAGS, 386 help="""\ 387 Maximum number of resources to list. The default is *unlimited*. 388 This flag interacts with other flags that are applied in this order: 389 *--flatten*, *--sort-by*, *--filter*, *--limit*. 390 """) 391 392PAGE_SIZE_FLAG = Argument( 393 '--page-size', 394 type=arg_parsers.BoundedInt(1, sys.maxsize, unlimited=True), 395 require_coverage_in_tests=False, 396 category=LIST_COMMAND_FLAGS, 397 help="""\ 398 Some services group resource list output into pages. This flag specifies 399 the maximum number of resources per page. The default is determined by the 400 service if it supports paging, otherwise it is *unlimited* (no paging). 401 Paging may be applied before or after *--filter* and *--limit* depending 402 on the service. 403 """) 404 405SORT_BY_FLAG = Argument( 406 '--sort-by', 407 metavar='FIELD', 408 type=arg_parsers.ArgList(), 409 require_coverage_in_tests=False, 410 category=LIST_COMMAND_FLAGS, 411 help="""\ 412 Comma-separated list of resource field key names to sort by. The 413 default order is ascending. Prefix a field with ``~'' for descending 414 order on that field. This flag interacts with other flags that are applied 415 in this order: *--flatten*, *--sort-by*, *--filter*, *--limit*. 416 """) 417 418URI_FLAG = Argument( 419 '--uri', 420 action='store_true', 421 require_coverage_in_tests=False, 422 category=LIST_COMMAND_FLAGS, 423 help="""\ 424 Print a list of resource URIs instead of the default output, and change the 425 command output to a list of URIs. If this flag is used with *--format*, 426 the formatting is applied on this URI list. To display URIs alongside other 427 keys instead, use the *uri()* transform. 428 """) 429 430# Binary Command Flags 431BINARY_BACKED_COMMAND_FLAGS = 'BINARY BACKED COMMAND' 432 433SHOW_EXEC_ERROR_FLAG = Argument( 434 '--show-exec-error', 435 hidden=True, 436 action='store_true', 437 required=False, 438 category=BINARY_BACKED_COMMAND_FLAGS, 439 help='If true and command fails, print the underlying command ' 440 'that was executed and its exit status.') 441 442 443class _Common(six.with_metaclass(abc.ABCMeta, object)): 444 """Base class for Command and Group.""" 445 category = None 446 _cli_generator = None 447 _is_hidden = False 448 _is_unicode_supported = False 449 _release_track = None 450 _valid_release_tracks = None 451 _notices = None 452 453 def __init__(self, is_group=False): 454 self.exit_code = 0 455 self.is_group = is_group 456 457 @staticmethod 458 def Args(parser): 459 """Set up arguments for this command. 460 461 Args: 462 parser: An argparse.ArgumentParser. 463 """ 464 pass 465 466 @staticmethod 467 def _Flags(parser): 468 """Adds subclass flags. 469 470 Args: 471 parser: An argparse.ArgumentParser object. 472 """ 473 pass 474 475 @classmethod 476 def IsHidden(cls): 477 return cls._is_hidden 478 479 @classmethod 480 def IsUnicodeSupported(cls): 481 if six.PY2: 482 return cls._is_unicode_supported 483 # We always support unicode on Python 3. 484 return True 485 486 @classmethod 487 def ReleaseTrack(cls): 488 return cls._release_track 489 490 @classmethod 491 def ValidReleaseTracks(cls): 492 return cls._valid_release_tracks 493 494 @classmethod 495 def GetTrackedAttribute(cls, obj, attribute): 496 """Gets the attribute value from obj for tracks. 497 498 The values are checked in ReleaseTrack._ALL order. 499 500 Args: 501 obj: The object to extract attribute from. 502 attribute: The attribute name in object. 503 504 Returns: 505 The attribute value from obj for tracks. 506 """ 507 for track in ReleaseTrack._ALL: # pylint: disable=protected-access 508 if track not in cls._valid_release_tracks: # pylint: disable=unsupported-membership-test 509 continue 510 names = [] 511 names.append(attribute + '_' + track.id) 512 if track.prefix: 513 names.append(attribute + '_' + track.prefix) 514 for name in names: 515 if hasattr(obj, name): 516 return getattr(obj, name) 517 return getattr(obj, attribute, None) 518 519 @classmethod 520 def Notices(cls): 521 return cls._notices 522 523 @classmethod 524 def AddNotice(cls, tag, msg, preserve_existing=False): 525 if not cls._notices: 526 cls._notices = {} 527 if tag in cls._notices and preserve_existing: 528 return 529 cls._notices[tag] = msg 530 531 @classmethod 532 def GetCLIGenerator(cls): 533 """Get a generator function that can be used to execute a gcloud command. 534 535 Returns: 536 A bound generator function to execute a gcloud command. 537 """ 538 if cls._cli_generator: 539 return cls._cli_generator.Generate 540 return None 541 542 543class Group(_Common): 544 """Group is a base class for groups to implement.""" 545 546 IS_COMMAND_GROUP = True 547 548 def __init__(self): 549 super(Group, self).__init__(is_group=True) 550 551 def Filter(self, context, args): 552 """Modify the context that will be given to this group's commands when run. 553 554 Args: 555 context: {str:object}, A set of key-value pairs that can be used for 556 common initialization among commands. 557 args: argparse.Namespace: The same namespace given to the corresponding 558 .Run() invocation. 559 """ 560 pass 561 562 563class Command(six.with_metaclass(abc.ABCMeta, _Common)): 564 """Command is a base class for commands to implement. 565 566 Attributes: 567 _cli_do_not_use_directly: calliope.cli.CLI, The CLI object representing this 568 command line tool. This should *only* be accessed via commands that 569 absolutely *need* introspection of the entire CLI. 570 context: {str:object}, A set of key-value pairs that can be used for 571 common initialization among commands. 572 _uri_cache_enabled: bool, The URI cache enabled state. 573 """ 574 575 IS_COMMAND = True 576 577 def __init__(self, cli, context): 578 super(Command, self).__init__(is_group=False) 579 self._cli_do_not_use_directly = cli 580 self.context = context 581 self._uri_cache_enabled = False 582 583 @property 584 def _cli_power_users_only(self): 585 return self._cli_do_not_use_directly 586 587 def ExecuteCommandDoNotUse(self, args): 588 """Execute a command using the given CLI. 589 590 Do not introduce new invocations of this method unless your command 591 *requires* it; any such new invocations must be approved by a team lead. 592 593 Args: 594 args: list of str, the args to Execute() via the CLI. 595 596 Returns: 597 pass-through of the return value from Execute() 598 """ 599 return self._cli_power_users_only.Execute(args, call_arg_complete=False) 600 601 @staticmethod 602 def _Flags(parser): 603 """Sets the default output format. 604 605 Args: 606 parser: The argparse parser. 607 """ 608 parser.display_info.AddFormat('default') 609 610 @abc.abstractmethod 611 def Run(self, args): 612 """Runs the command. 613 614 Args: 615 args: argparse.Namespace, An object that contains the values for the 616 arguments specified in the .Args() method. 617 618 Returns: 619 A resource object dispatched by display.Displayer(). 620 """ 621 pass 622 623 def Epilog(self, resources_were_displayed): 624 """Called after resources are displayed if the default format was used. 625 626 Args: 627 resources_were_displayed: True if resources were displayed. 628 """ 629 _ = resources_were_displayed 630 631 def GetReferencedKeyNames(self, args): 632 """Returns the key names referenced by the filter and format expressions.""" 633 return display.Displayer(self, args, None).GetReferencedKeyNames() 634 635 def GetUriFunc(self): 636 """Returns a function that transforms a command resource item to a URI. 637 638 Returns: 639 func(resource) that transforms resource into a URI. 640 """ 641 return None 642 643 644class TopicCommand(six.with_metaclass(abc.ABCMeta, Command)): 645 """A command that displays its own help on execution.""" 646 647 def Run(self, args): 648 self.ExecuteCommandDoNotUse(args.command_path[1:] + 649 ['--document=style=topic']) 650 return None 651 652 653class SilentCommand(six.with_metaclass(abc.ABCMeta, Command)): 654 """A command that produces no output.""" 655 656 @staticmethod 657 def _Flags(parser): 658 parser.display_info.AddFormat('none') 659 660 661class DescribeCommand(six.with_metaclass(abc.ABCMeta, Command)): 662 """A command that prints one resource in the 'default' format.""" 663 664 665class ImportCommand(six.with_metaclass(abc.ABCMeta, Command)): 666 """A command that imports one resource from yaml format.""" 667 668 669class ExportCommand(six.with_metaclass(abc.ABCMeta, Command)): 670 """A command that outputs one resource to file in yaml format.""" 671 672 673class DeclarativeCommand(six.with_metaclass(abc.ABCMeta, Command)): 674 """Command class for managing gcp resources as YAML/JSON files.""" 675 676 677class BinaryBackedCommand(six.with_metaclass(abc.ABCMeta, Command)): 678 """A command that wraps a BinaryBackedOperation.""" 679 680 @staticmethod 681 def _Flags(parser): 682 SHOW_EXEC_ERROR_FLAG.AddToParser(parser) 683 684 @staticmethod 685 def _DefaultOperationResponseHandler(response): 686 """Process results of BinaryOperation Execution.""" 687 if response.stdout: 688 log.Print(response.stdout) 689 690 if response.stderr: 691 log.status.Print(response.stderr) 692 693 if response.failed: 694 return None 695 696 return response.stdout 697 698 699class CacheCommand(six.with_metaclass(abc.ABCMeta, Command)): 700 """A command that affects the resource URI cache.""" 701 702 def __init__(self, *args, **kwargs): 703 super(CacheCommand, self).__init__(*args, **kwargs) 704 self._uri_cache_enabled = True 705 706 707class ListCommand(six.with_metaclass(abc.ABCMeta, CacheCommand)): 708 """A command that pretty-prints all resources.""" 709 710 @staticmethod 711 def _Flags(parser): 712 """Adds the default flags for all ListCommand commands. 713 714 Args: 715 parser: The argparse parser. 716 """ 717 718 FILTER_FLAG.AddToParser(parser) 719 LIMIT_FLAG.AddToParser(parser) 720 PAGE_SIZE_FLAG.AddToParser(parser) 721 SORT_BY_FLAG.AddToParser(parser) 722 URI_FLAG.AddToParser(parser) 723 parser.display_info.AddFormat('default') 724 725 def Epilog(self, resources_were_displayed): 726 """Called after resources are displayed if the default format was used. 727 728 Args: 729 resources_were_displayed: True if resources were displayed. 730 """ 731 if not resources_were_displayed: 732 log.status.Print('Listed 0 items.') 733 734 735class CreateCommand(CacheCommand, SilentCommand): 736 """A command that creates resources.""" 737 738 739class DeleteCommand(CacheCommand, SilentCommand): 740 """A command that deletes resources.""" 741 742 743class RestoreCommand(CacheCommand, SilentCommand): 744 """A command that restores resources.""" 745 746 747class UpdateCommand(SilentCommand): 748 """A command that updates resources.""" 749 750 pass 751 752 753def Hidden(cmd_class): 754 """Decorator for hiding calliope commands and groups. 755 756 Decorate a subclass of base.Command or base.Group with this function, and the 757 decorated command or group will not show up in help text. 758 759 Args: 760 cmd_class: base._Common, A calliope command or group. 761 762 Returns: 763 A modified version of the provided class. 764 """ 765 # pylint: disable=protected-access 766 cmd_class._is_hidden = True 767 return cmd_class 768 769 770def UnicodeIsSupported(cmd_class): 771 """Decorator for calliope commands and groups that support unicode. 772 773 Decorate a subclass of base.Command or base.Group with this function, and the 774 decorated command or group will not raise the argparse unicode command line 775 argument exception. 776 777 Args: 778 cmd_class: base._Common, A calliope command or group. 779 780 Returns: 781 A modified version of the provided class. 782 """ 783 # pylint: disable=protected-access 784 cmd_class._is_unicode_supported = True 785 return cmd_class 786 787 788def ReleaseTracks(*tracks): 789 """Mark this class as the command implementation for the given release tracks. 790 791 Args: 792 *tracks: [ReleaseTrack], A list of release tracks that this is valid for. 793 794 Returns: 795 The decorated function. 796 """ 797 def ApplyReleaseTracks(cmd_class): 798 """Wrapper function for the decorator.""" 799 # pylint: disable=protected-access 800 cmd_class._valid_release_tracks = set(tracks) 801 return cmd_class 802 return ApplyReleaseTracks 803 804 805def Deprecate(is_removed=True, 806 warning='This command is deprecated.', 807 error='This command has been removed.'): 808 """Decorator that marks a Calliope command as deprecated. 809 810 Decorate a subclass of base.Command with this function and the 811 decorated command will be modified as follows: 812 813 - If is_removed is false, a warning will be logged when *command* is run, 814 otherwise an *exception* will be thrown containing error message 815 816 -Command help output will be modified to include warning/error message 817 depending on value of is_removed 818 819 - Command help text will automatically hidden from the reference documentation 820 (e.g. @base.Hidden) if is_removed is True 821 822 823 Args: 824 is_removed: boolean, True if the command should raise an error 825 when executed. If false, a warning is printed 826 warning: string, warning message 827 error: string, error message 828 829 Returns: 830 A modified version of the provided class. 831 """ 832 833 def DeprecateCommand(cmd_class): 834 """Wrapper Function that creates actual decorated class. 835 836 Args: 837 cmd_class: base.Command or base.Group subclass to be decorated 838 839 Returns: 840 The decorated class. 841 """ 842 if is_removed: 843 msg = error 844 deprecation_tag = '{0}(REMOVED){0} '.format(MARKDOWN_BOLD) 845 else: 846 msg = warning 847 deprecation_tag = '{0}(DEPRECATED){0} '.format(MARKDOWN_BOLD) 848 849 cmd_class.AddNotice(deprecation_tag, msg) 850 851 def RunDecorator(run_func): 852 @wraps(run_func) 853 def WrappedRun(*args, **kw): 854 if is_removed: 855 raise DeprecationException(error) 856 log.warning(warning) 857 return run_func(*args, **kw) 858 return WrappedRun 859 860 if issubclass(cmd_class, Group): 861 cmd_class.Filter = RunDecorator(cmd_class.Filter) 862 else: 863 cmd_class.Run = RunDecorator(cmd_class.Run) 864 865 if is_removed: 866 return Hidden(cmd_class) 867 868 return cmd_class 869 870 return DeprecateCommand 871 872 873def _ChoiceValueType(value): 874 """Returns a function that ensures choice flag values match Cloud SDK Style. 875 876 Args: 877 value: string, string representing flag choice value parsed from command 878 line. 879 880 Returns: 881 A string value entirely in lower case, with words separated by 882 hyphens. 883 """ 884 return value.replace('_', '-').lower() 885 886 887def ChoiceArgument(name_or_flag, choices, help_str=None, required=False, 888 action=None, metavar=None, dest=None, default=None, 889 hidden=False): 890 """Returns Argument with a Cloud SDK style compliant set of choices. 891 892 Args: 893 name_or_flag: string, Either a name or a list of option strings, 894 e.g. foo or -f, --foo. 895 choices: container, A container (e.g. set, dict, list, tuple) of the 896 allowable values for the argument. Should consist of strings entirely in 897 lower case, with words separated by hyphens. 898 help_str: string, A brief description of what the argument does. 899 required: boolean, Whether or not the command-line option may be omitted. 900 action: string or argparse.Action, The basic type of argeparse.action 901 to be taken when this argument is encountered at the command line. 902 metavar: string, A name for the argument in usage messages. 903 dest: string, The name of the attribute to be added to the object returned 904 by parse_args(). 905 default: string, The value produced if the argument is absent from the 906 command line. 907 hidden: boolean, Whether or not the command-line option is hidden. 908 909 Returns: 910 Argument object with choices, that can accept both lowercase and uppercase 911 user input with hyphens or undersores. 912 913 Raises: 914 TypeError: If choices are not an iterable container of string options. 915 ValueError: If provided choices are not Cloud SDK Style compliant. 916 """ 917 918 if not choices: 919 raise ValueError('Choices must not be empty.') 920 921 if (not isinstance(choices, collections.Iterable) 922 or isinstance(choices, six.string_types)): 923 raise TypeError( 924 'Choices must be an iterable container of options: [{}].'.format( 925 ', '.join(choices))) 926 927 # Valid choices should be alphanumeric sequences followed by an optional 928 # period '.', separated by a single hyphen '-'. 929 choice_re = re.compile(r'^([a-z0-9]\.?-?)+[a-z0-9]$') 930 invalid_choices = [x for x in choices if not choice_re.match(x)] 931 if invalid_choices: 932 raise ValueError( 933 ('Invalid choices [{}]. Choices must be entirely in lowercase with ' 934 'words separated by hyphens(-)').format(', '.join(invalid_choices))) 935 936 return Argument(name_or_flag, choices=choices, required=required, 937 type=_ChoiceValueType, help=help_str, action=action, 938 metavar=metavar, dest=dest, default=default, hidden=hidden) 939 940 941def DisableUserProjectQuota(): 942 """Disable the quota header if the user hasn't manually specified it.""" 943 if not properties.VALUES.billing.quota_project.IsExplicitlySet(): 944 properties.VALUES.billing.quota_project.Set( 945 properties.VALUES.billing.LEGACY) 946 947 948def EnableUserProjectQuota(): 949 """Enable the quota header for current project.""" 950 properties.VALUES.billing.quota_project.Set( 951 properties.VALUES.billing.CURRENT_PROJECT) 952 953 954def EnableUserProjectQuotaWithFallback(): 955 """Tries the current project and fall back to the legacy mode.""" 956 properties.VALUES.billing.quota_project.Set( 957 properties.VALUES.billing.CURRENT_PROJECT_WITH_FALLBACK) 958 959 960def UserProjectQuotaWithFallbackEnabled(): 961 """Returns if the CURRENT_PROJECT_WITH_FALLBACK mode is enabled.""" 962 return properties.VALUES.billing.quota_project.Get( 963 ) == properties.VALUES.billing.CURRENT_PROJECT_WITH_FALLBACK 964 965 966def OptOutRequests(): 967 """Opts the command group out of using requests to make HTTP requests. 968 969 Call this function in the Filter method of the command group 970 to disable requests. 971 """ 972 properties.VALUES.transport.opt_out_requests.Set(True) 973 974 975def UseRequests(): 976 """Returns True if using requests to make HTTP requests. 977 978 transport/disable_requests_override is a global switch to turn off requests in 979 case support is buggy. transport/opt_out_requests is an internal property 980 to opt surfaces out of requests. 981 """ 982 983 return (UseGoogleAuth() and 984 not properties.VALUES.transport.opt_out_requests.GetBool() and 985 not properties.VALUES.transport.disable_requests_override.GetBool()) 986 987 988def OptOutGoogleAuth(): 989 """Opt-out the command group to use google auth for authentication. 990 991 Call this function in the Filter method of the command group 992 to opt-out google-auth. 993 """ 994 properties.VALUES.auth.opt_out_google_auth.Set(True) 995 996 997def UseGoogleAuth(): 998 """Returns True if using google-auth to authenticate the http request. 999 1000 auth/disable_load_google_auth is a global switch to turn off google-auth in 1001 case google-auth is crashing. auth/opt_out_google_auth is an internal property 1002 to opt-out a surface. 1003 """ 1004 return not (properties.VALUES.auth.opt_out_google_auth.GetBool() or 1005 properties.VALUES.auth.disable_load_google_auth.GetBool()) 1006 1007 1008def LogCommand(prog, args): 1009 """Log (to debug) the command/arguments being run in a standard format. 1010 1011 `gcloud feedback` depends on this format. 1012 1013 Example format is: 1014 1015 Running [gcloud.example.command] with arguments: [--bar: "baz"] 1016 1017 Args: 1018 prog: string, the dotted name of the command being run (ex. 1019 "gcloud.foos.list") 1020 args: argparse.namespace, the parsed arguments from the command line 1021 """ 1022 specified_args = sorted(six.iteritems(args.GetSpecifiedArgs())) 1023 arg_string = ', '.join(['{}: "{}"'.format(k, v) for k, v in specified_args]) 1024 log.debug('Running [{}] with arguments: [{}]'.format(prog, arg_string)) 1025