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