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