1# -*- coding: utf-8 -*- #
2# Copyright 2019 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"""Flags and helpers for the compute related commands."""
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import unicode_literals
21
22import copy
23import functools
24
25from googlecloudsdk.api_lib.compute import filter_rewrite
26from googlecloudsdk.api_lib.compute.regions import service as regions_service
27from googlecloudsdk.api_lib.compute.zones import service as zones_service
28from googlecloudsdk.calliope import actions
29from googlecloudsdk.calliope import arg_parsers
30from googlecloudsdk.command_lib.compute import completers
31from googlecloudsdk.command_lib.compute import scope as compute_scope
32from googlecloudsdk.command_lib.compute import scope_prompter
33from googlecloudsdk.core import exceptions
34from googlecloudsdk.core import log
35from googlecloudsdk.core import properties
36from googlecloudsdk.core import resources
37from googlecloudsdk.core.console import console_io
38from googlecloudsdk.core.resource import resource_projection_spec
39from googlecloudsdk.core.util import text
40import six
41
42ZONE_PROPERTY_EXPLANATION = """\
43If not specified and the ``compute/zone'' property isn't set, you
44may be prompted to select a zone (interactive mode only).
45
46To avoid prompting when this flag is omitted, you can set the
47``compute/zone'' property:
48
49  $ gcloud config set compute/zone ZONE
50
51A list of zones can be fetched by running:
52
53  $ gcloud compute zones list
54
55To unset the property, run:
56
57  $ gcloud config unset compute/zone
58
59Alternatively, the zone can be stored in the environment variable
60``CLOUDSDK_COMPUTE_ZONE''.
61"""
62
63ZONE_PROPERTY_EXPLANATION_NO_DEFAULT = """\
64If not specified, you may be prompted to select a zone (interactive mode only).
65
66A list of zones can be fetched by running:
67
68  $ gcloud compute zones list
69"""
70
71REGION_PROPERTY_EXPLANATION = """\
72If not specified, you may be prompted to select a region (interactive mode only).
73
74To avoid prompting when this flag is omitted, you can set the
75``compute/region'' property:
76
77  $ gcloud config set compute/region REGION
78
79A list of regions can be fetched by running:
80
81  $ gcloud compute regions list
82
83To unset the property, run:
84
85  $ gcloud config unset compute/region
86
87Alternatively, the region can be stored in the environment
88variable ``CLOUDSDK_COMPUTE_REGION''.
89"""
90
91REGION_PROPERTY_EXPLANATION_NO_DEFAULT = """\
92If not specified, you may be prompted to select a region (interactive mode only).
93
94A list of regions can be fetched by running:
95
96  $ gcloud compute regions list
97"""
98
99
100class ScopesFetchingException(exceptions.Error):
101  pass
102
103
104class BadArgumentException(ValueError):
105  """Unhandled error for validating function arguments."""
106  pass
107
108
109def AddZoneFlag(parser, resource_type, operation_type, flag_prefix=None,
110                explanation=ZONE_PROPERTY_EXPLANATION, help_text=None,
111                hidden=False, plural=False, custom_plural=None):
112  """Adds a --zone flag to the given parser.
113
114  Args:
115    parser: argparse parser.
116    resource_type: str, human readable name for the resource type this flag is
117                   qualifying, for example "instance group".
118    operation_type: str, human readable name for the operation, for example
119                    "update" or "delete".
120    flag_prefix: str, flag will be named --{flag_prefix}-zone.
121    explanation: str, detailed explanation of the flag.
122    help_text: str, help text will be overridden with this value.
123    hidden: bool, If True, --zone argument help will be hidden.
124    plural: bool, resource_type will be pluralized or not depending on value.
125    custom_plural: str, If plural is True then this string will be used as
126                        resource types, otherwise resource_types will be
127                        pluralized by appending 's'.
128  """
129  short_help = 'Zone of the {0} to {1}.'.format(
130      text.Pluralize(
131          int(plural) + 1, resource_type or '', custom_plural), operation_type)
132  flag_name = 'zone'
133  if flag_prefix is not None:
134    flag_name = flag_prefix + '-' + flag_name
135  parser.add_argument(
136      '--' + flag_name,
137      hidden=hidden,
138      completer=completers.ZonesCompleter,
139      action=actions.StoreProperty(properties.VALUES.compute.zone),
140      help=help_text or '{0} {1}'.format(short_help, explanation))
141
142
143def AddRegionFlag(parser, resource_type, operation_type,
144                  flag_prefix=None,
145                  explanation=REGION_PROPERTY_EXPLANATION, help_text=None,
146                  hidden=False, plural=False, custom_plural=None):
147  """Adds a --region flag to the given parser.
148
149  Args:
150    parser: argparse parser.
151    resource_type: str, human readable name for the resource type this flag is
152                   qualifying, for example "instance group".
153    operation_type: str, human readable name for the operation, for example
154                    "update" or "delete".
155    flag_prefix: str, flag will be named --{flag_prefix}-region.
156    explanation: str, detailed explanation of the flag.
157    help_text: str, help text will be overridden with this value.
158    hidden: bool, If True, --region argument help will be hidden.
159    plural: bool, resource_type will be pluralized or not depending on value.
160    custom_plural: str, If plural is True then this string will be used as
161                        resource types, otherwise resource_types will be
162                        pluralized by appending 's'.
163  """
164  short_help = 'Region of the {0} to {1}.'.format(
165      text.Pluralize(
166          int(plural) + 1, resource_type or '', custom_plural), operation_type)
167  flag_name = 'region'
168  if flag_prefix is not None:
169    flag_name = flag_prefix + '-' + flag_name
170  parser.add_argument(
171      '--' + flag_name,
172      completer=completers.RegionsCompleter,
173      action=actions.StoreProperty(properties.VALUES.compute.region),
174      hidden=hidden,
175      help=help_text or '{0} {1}'.format(short_help, explanation))
176
177
178class UnderSpecifiedResourceError(exceptions.Error):
179  """Raised when argument is required additional scope to be resolved."""
180
181  def __init__(self, underspecified_names, flag_names):
182    phrases = ('one of ', 'flags') if len(flag_names) > 1 else ('', 'flag')
183    super(UnderSpecifiedResourceError, self).__init__(
184        'Underspecified resource [{3}]. Specify {0}the [{1}] {2}.'
185        .format(phrases[0],
186                ', '.join(sorted(flag_names)),
187                phrases[1],
188                ', '.join(underspecified_names)))
189
190
191class ResourceStub(object):
192  """Interface used by scope listing to report scope names."""
193
194  def __init__(self, name, deprecated=None):
195    self.name = name
196    self.deprecated = deprecated
197
198
199def GetDefaultScopeLister(compute_client, project=None):
200  """Constructs default zone/region lister."""
201  scope_func = {
202      compute_scope.ScopeEnum.ZONE:
203          functools.partial(zones_service.List, compute_client),
204      compute_scope.ScopeEnum.REGION:
205          functools.partial(regions_service.List, compute_client),
206      compute_scope.ScopeEnum.GLOBAL: lambda _: [ResourceStub(name='')]
207  }
208  def Lister(scopes, _):
209    prj = project or properties.VALUES.core.project.Get(required=True)
210    results = {}
211    for scope in scopes:
212      results[scope] = scope_func[scope](prj)
213    return results
214  return Lister
215
216
217class ResourceArgScope(object):
218  """Facilitates mapping of scope, flag and collection."""
219
220  def __init__(self, scope, flag_prefix, collection):
221    self.scope_enum = scope
222    if flag_prefix:
223      flag_prefix = flag_prefix.replace('-', '_')
224      if scope is compute_scope.ScopeEnum.GLOBAL:
225        self.flag_name = scope.flag_name + '_' + flag_prefix
226      else:
227        self.flag_name = flag_prefix + '_' + scope.flag_name
228    else:
229      self.flag_name = scope.flag_name
230    self.flag = '--' + self.flag_name.replace('_', '-')
231    self.collection = collection
232
233
234class ResourceArgScopes(object):
235  """Represents chosen set of scopes."""
236
237  def __init__(self, flag_prefix):
238    self.flag_prefix = flag_prefix
239    self.scopes = {}
240
241  def AddScope(self, scope, collection):
242    self.scopes[scope] = ResourceArgScope(scope, self.flag_prefix, collection)
243
244  def SpecifiedByArgs(self, args):
245    """Given argparse args return selected scope and its value."""
246    for resource_scope in six.itervalues(self.scopes):
247      scope_value = getattr(args, resource_scope.flag_name, None)
248      if scope_value is not None:
249        return resource_scope, scope_value
250    return None, None
251
252  def GetImplicitScope(self, default_scope=None):
253    """See if there is no ambiguity even if scope is not known from args."""
254    if len(self.scopes) == 1:
255      return next(six.itervalues(self.scopes))
256    return default_scope
257
258  def __iter__(self):
259    return iter(six.itervalues(self.scopes))
260
261  def __contains__(self, scope):
262    return scope in self.scopes
263
264  def __getitem__(self, scope):
265    return self.scopes[scope]
266
267  def __len__(self):
268    return len(self.scopes)
269
270
271class ResourceResolver(object):
272  """Object responsible for resolving resources.
273
274  There are two ways to build an instance of this object:
275  1. Preferred when you don't have instance of ResourceArgScopes already built,
276     using .FromMap static function. For example:
277
278     resolver = ResourceResolver.FromMap(
279         'instance',
280         {compute_scope.ScopeEnum.ZONE: 'compute.instances'})
281
282     where:
283     - 'instance' is human readable name of the resource,
284     - dictionary maps allowed scope (in this case only zone) to resource types
285       in those scopes.
286     - optional prefix of scope flags was skipped.
287
288  2. Using constructor. Recommended only if you have instance of
289     ResourceArgScopes available.
290
291  Once you've built the resover you can use it to build resource references (and
292  prompt for scope if it was not specified):
293
294  resolver.ResolveResources(
295        instance_name, compute_scope.ScopeEnum.ZONE,
296        instance_zone, self.resources,
297        scope_lister=flags.GetDefaultScopeLister(
298            self.compute_client, self.project))
299
300  will return a list of instances (of length 0 or 1 in this case, because we
301  pass a name of single instance or None). It will prompt if and only if
302  instance_name was not None but instance_zone was None.
303
304  scope_lister is necessary for prompting.
305  """
306
307  def __init__(self, scopes, resource_name):
308    """Initilize ResourceResolver instance.
309
310    Prefer building with FromMap unless you have ResourceArgScopes object
311    already built.
312
313    Args:
314      scopes: ResourceArgScopes, allowed scopes and resource types in those
315              scopes.
316      resource_name: str, human readable name for resources eg
317                     "instance group".
318    """
319    self.scopes = scopes
320    self.resource_name = resource_name
321
322  @staticmethod
323  def FromMap(resource_name, scopes_map, scope_flag_prefix=None):
324    """Initilize ResourceResolver instance.
325
326    Args:
327      resource_name: str, human readable name for resources eg
328                     "instance group".
329      scopes_map: dict, with keys should be instances of ScopeEnum, values
330              should be instances of ResourceArgScope.
331      scope_flag_prefix: str, prefix of flags specyfying scope.
332    Returns:
333      New instance of ResourceResolver.
334    """
335    scopes = ResourceArgScopes(flag_prefix=scope_flag_prefix)
336    for scope, resource in six.iteritems(scopes_map):
337      scopes.AddScope(scope, resource)
338    return ResourceResolver(scopes, resource_name)
339
340  def _ValidateNames(self, names):
341    if not isinstance(names, list):
342      raise BadArgumentException(
343          "Expected names to be a list but it is '{0}'".format(names))
344
345  def _ValidateDefaultScope(self, default_scope):
346    if default_scope is not None and default_scope not in self.scopes:
347      raise BadArgumentException(
348          'Unexpected value for default_scope {0}, expected None or {1}'
349          .format(default_scope,
350                  ' or '.join([s.scope_enum.name for s in self.scopes])))
351
352  def _GetResourceScopeParam(self,
353                             resource_scope,
354                             scope_value,
355                             project,
356                             api_resource_registry,
357                             with_project=True):
358    """Gets the resource scope parameters."""
359
360    if scope_value is not None:
361      if resource_scope.scope_enum == compute_scope.ScopeEnum.GLOBAL:
362        return None
363      else:
364        collection = compute_scope.ScopeEnum.CollectionForScope(
365            resource_scope.scope_enum)
366        if with_project:
367          return api_resource_registry.Parse(
368              scope_value, params={
369                  'project': project
370              }, collection=collection).Name()
371        else:
372          return api_resource_registry.Parse(
373              scope_value, params={}, collection=collection).Name()
374    else:
375      if resource_scope and (resource_scope.scope_enum !=
376                             compute_scope.ScopeEnum.GLOBAL):
377        return resource_scope.scope_enum.property_func
378
379  def _GetRefsAndUnderspecifiedNames(
380      self, names, params, collection, scope_defined, api_resource_registry):
381    """Returns pair of lists: resolved references and unresolved names.
382
383    Args:
384      names: list of names to attempt resolving
385      params: params given when attempting to resolve references
386      collection: collection for the names
387      scope_defined: bool, whether scope is known
388      api_resource_registry: Registry object
389    """
390    refs = []
391    underspecified_names = []
392    for name in names:
393      try:
394        # Make each element an array so that we can do in place updates.
395        ref = [api_resource_registry.Parse(name, params=params,
396                                           collection=collection,
397                                           enforce_collection=False)]
398      except (resources.UnknownCollectionException,
399              resources.RequiredFieldOmittedException,
400              properties.RequiredPropertyError):
401        if scope_defined:
402          raise
403        ref = [name]
404        underspecified_names.append(ref)
405      refs.append(ref)
406    return refs, underspecified_names
407
408  def _ResolveMultiScope(self, with_project, project, underspecified_names,
409                         api_resource_registry, refs):
410    """Resolve argument against available scopes of the resource."""
411    names = copy.deepcopy(underspecified_names)
412    for scope in self.scopes:
413      if with_project:
414        params = {
415            'project': project,
416        }
417      else:
418        params = {}
419      params[scope.scope_enum.param_name] = scope.scope_enum.property_func
420      for name in names:
421        try:
422          ref = [api_resource_registry.Parse(name[0], params=params,
423                                             collection=scope.collection,
424                                             enforce_collection=False)]
425          refs.remove(name)
426          refs.append(ref)
427          underspecified_names.remove(name)
428        except (resources.UnknownCollectionException,
429                resources.RequiredFieldOmittedException,
430                properties.RequiredPropertyError,
431                ValueError):
432          continue
433
434  def _ResolveUnderspecifiedNames(self,
435                                  underspecified_names,
436                                  default_scope,
437                                  scope_lister,
438                                  project,
439                                  api_resource_registry,
440                                  with_project=True):
441    """Attempt to resolve scope for unresolved names.
442
443    If unresolved_names was generated with _GetRefsAndUnderspecifiedNames
444    changing them will change corresponding elements of refs list.
445
446    Args:
447      underspecified_names: list of one-items lists containing str
448      default_scope: default scope for the resources
449      scope_lister: callback used to list potential scopes for the resources
450      project: str, id of the project
451      api_resource_registry: resources Registry
452      with_project: indicates whether or not project is associated. It should be
453        False for flexible resource APIs
454
455    Raises:
456      UnderSpecifiedResourceError: when resource scope can't be resolved.
457    """
458    if not underspecified_names:
459      return
460
461    names = [n[0] for n in underspecified_names]
462
463    if not console_io.CanPrompt():
464      raise UnderSpecifiedResourceError(names, [s.flag for s in self.scopes])
465
466    resource_scope_enum, scope_value = scope_prompter.PromptForScope(
467        self.resource_name, names, [s.scope_enum for s in self.scopes],
468        default_scope.scope_enum if default_scope is not None else None,
469        scope_lister)
470    if resource_scope_enum is None:
471      raise UnderSpecifiedResourceError(names, [s.flag for s in self.scopes])
472
473    resource_scope = self.scopes[resource_scope_enum]
474    if with_project:
475      params = {
476          'project': project,
477      }
478    else:
479      params = {}
480
481    if resource_scope.scope_enum != compute_scope.ScopeEnum.GLOBAL:
482      params[resource_scope.scope_enum.param_name] = scope_value
483
484    for name in underspecified_names:
485      name[0] = api_resource_registry.Parse(
486          name[0],
487          params=params,
488          collection=resource_scope.collection,
489          enforce_collection=True)
490
491  def ResolveResources(self,
492                       names,
493                       resource_scope,
494                       scope_value,
495                       api_resource_registry,
496                       default_scope=None,
497                       scope_lister=None,
498                       with_project=True):
499    """Resolve this resource against the arguments.
500
501    Args:
502      names: list of str, list of resource names
503      resource_scope: ScopeEnum, kind of scope of resources; if this is not None
504                   scope_value should be name of scope of type specified by this
505                   argument. If this is None scope_value should be None, in that
506                   case if prompting is possible user will be prompted to
507                   select scope (if prompting is forbidden it will raise an
508                   exception).
509      scope_value: ScopeEnum, scope of resources; if this is not None
510                   resource_scope should be type of scope specified by this
511                   argument. If this is None resource_scope should be None, in
512                   that case if prompting is possible user will be prompted to
513                   select scope (if prompting is forbidden it will raise an
514                   exception).
515      api_resource_registry: instance of core.resources.Registry.
516      default_scope: ScopeEnum, ZONE, REGION, GLOBAL, or None when resolving
517          name and scope was not specified use this as default. If there is
518          exactly one possible scope it will be used, there is no need to
519          specify default_scope.
520      scope_lister: func(scope, underspecified_names), a callback which returns
521        list of items (with 'name' attribute) for given scope.
522      with_project: indicates whether or not project is associated. It should be
523        False for flexible resource APIs.
524    Returns:
525      Resource reference or list of references if plural.
526    Raises:
527      BadArgumentException: when names is not a list or default_scope is not one
528          of the configured scopes.
529      UnderSpecifiedResourceError: if it was not possible to resolve given names
530          as resources references.
531    """
532    self._ValidateNames(names)
533    self._ValidateDefaultScope(default_scope)
534    if resource_scope is not None:
535      resource_scope = self.scopes[resource_scope]
536    if default_scope is not None:
537      default_scope = self.scopes[default_scope]
538    project = properties.VALUES.core.project.GetOrFail
539    if with_project:
540      params = {
541          'project': project,
542      }
543    else:
544      params = {}
545    if scope_value is None:
546      resource_scope = self.scopes.GetImplicitScope(default_scope)
547
548    resource_scope_param = self._GetResourceScopeParam(
549        resource_scope,
550        scope_value,
551        project,
552        api_resource_registry,
553        with_project=with_project)
554    if resource_scope_param is not None:
555      params[resource_scope.scope_enum.param_name] = resource_scope_param
556
557    collection = resource_scope and resource_scope.collection
558
559    # See if we can resolve names with so far deduced scope and its value.
560    refs, underspecified_names = self._GetRefsAndUnderspecifiedNames(
561        names, params, collection, scope_value is not None,
562        api_resource_registry)
563
564    # Try to resolve with each available scope
565    if underspecified_names and len(self.scopes) > 1:
566      self._ResolveMultiScope(with_project, project, underspecified_names,
567                              api_resource_registry, refs)
568
569    # If we still have some resources which need to be resolve see if we can
570    # prompt the user and try to resolve these again.
571    self._ResolveUnderspecifiedNames(
572        underspecified_names,
573        default_scope,
574        scope_lister,
575        project,
576        api_resource_registry,
577        with_project=with_project)
578
579    # Now unpack each element.
580    refs = [ref[0] for ref in refs]
581
582    # Make sure correct collection was given for each resource, for example
583    # URLs have implicit collections.
584    expected_collections = [scope.collection for scope in self.scopes]
585    for ref in refs:
586      if ref.Collection() not in expected_collections:
587        raise resources.WrongResourceCollectionException(
588            expected=','.join(expected_collections),
589            got=ref.Collection(),
590            path=ref.SelfLink())
591    return refs
592
593
594class ResourceArgument(object):
595  """Encapsulates concept of compute resource as command line argument.
596
597  Basic Usage:
598    class MyCommand(base.Command):
599      _BACKEND_SERVICE_ARG = flags.ResourceArgument(
600          resource_name='backend service',
601          completer=compute_completers.BackendServiceCompleter,
602          regional_collection='compute.regionBackendServices',
603          global_collection='compute.backendServices')
604      _INSTANCE_GROUP_ARG = flags.ResourceArgument(
605          resource_name='instance group',
606          completer=compute_completers.InstanceGroupsCompleter,
607          zonal_collection='compute.instanceGroups',)
608
609      @staticmethod
610      def Args(parser):
611        MyCommand._BACKEND_SERVICE_ARG.AddArgument(parser)
612        MyCommand._INSTANCE_GROUP_ARG.AddArgument(parser)
613
614      def Run(args):
615        api_resource_registry = resources.REGISTRY.CloneAndSwitch(
616            api_tools_client)
617        backend_service_ref = _BACKEND_SERVICE_ARG.ResolveAsResource(
618            args, api_resource_registry, default_scope=flags.ScopeEnum.GLOBAL)
619        instance_group_ref = _INSTANCE_GROUP_ARG.ResolveAsResource(
620            args, api_resource_registry, default_scope=flags.ScopeEnum.ZONE)
621        ...
622
623    In the above example the following five arguments/flags will be defined:
624      NAME - positional for backend service
625      --region REGION to qualify backend service
626      --global  to qualify backend service
627      --instance-group INSTANCE_GROUP name for the instance group
628      --instance-group-zone INSTANCE_GROUP_ZONE further qualifies instance group
629
630    More generally this construct can simultaneously support global, regional
631    and zonal qualifiers (or any combination of) for each resource.
632  """
633
634  def __init__(self,
635               name=None,
636               resource_name=None,
637               completer=None,
638               plural=False,
639               required=True,
640               zonal_collection=None,
641               regional_collection=None,
642               global_collection=None,
643               global_help_text=None,
644               region_explanation=None,
645               region_help_text=None,
646               region_hidden=False,
647               zone_explanation=None,
648               zone_help_text=None,
649               zone_hidden=False,
650               short_help=None,
651               detailed_help=None,
652               custom_plural=None,
653               use_existing_default_scope=None):
654
655    """Constructor.
656
657    Args:
658      name: str, argument name.
659      resource_name: str, human readable name for resources eg "instance group".
660      completer: completion_cache.Completer, The completer class type.
661      plural: bool, whether to accept multiple values.
662      required: bool, whether this argument is required.
663      zonal_collection: str, include zone flag and use this collection
664                             to resolve it.
665      regional_collection: str, include region flag and use this collection
666                                to resolve it.
667      global_collection: str, if also zonal and/or regional adds global flag
668                              and uses this collection to resolve as
669                              global resource.
670      global_help_text: str, if provided, global flag help text will be
671                             overridden with this value.
672      region_explanation: str, long help that will be given for region flag,
673                               empty by default.
674      region_help_text: str, if provided, region flag help text will be
675                             overridden with this value.
676      region_hidden: bool, Hide region in help if True.
677      zone_explanation: str, long help that will be given for zone flag, empty
678                             by default.
679      zone_help_text: str, if provided, zone flag help text will be overridden
680                           with this value.
681      zone_hidden: bool, Hide region in help if True.
682      short_help: str, help for the flag being added, if not provided help text
683                       will be 'The name[s] of the ${resource_name}[s].'.
684      detailed_help: str, detailed help for the flag being added, if not
685                          provided there will be no detailed help for the flag.
686      custom_plural: str, If plural is True then this string will be used as
687                          plural resource name.
688      use_existing_default_scope: bool, when set to True, already existing
689                                  zone and/or region flags will be used for
690                                  this argument.
691
692    Raises:
693      exceptions.Error: if there some inconsistency in arguments.
694    """
695    self.name_arg = name or 'name'
696    self._short_help = short_help
697    self._detailed_help = detailed_help
698    self.use_existing_default_scope = use_existing_default_scope
699    if self.name_arg.startswith('--'):
700      self.is_flag = True
701      self.name = self.name_arg[2:].replace('-', '_')
702      flag_prefix = (None if self.use_existing_default_scope else
703                     self.name_arg[2:])
704      self.scopes = ResourceArgScopes(flag_prefix=flag_prefix)
705    else:  # positional
706      self.scopes = ResourceArgScopes(flag_prefix=None)
707      self.name = self.name_arg  # arg name is same as its spec.
708    self.resource_name = resource_name
709    self.completer = completer
710    self.plural = plural
711    self.custom_plural = custom_plural
712    self.required = required
713    if not (zonal_collection or regional_collection or global_collection):
714      raise exceptions.Error('Must specify at least one resource type zonal, '
715                             'regional or global')
716    if zonal_collection:
717      self.scopes.AddScope(compute_scope.ScopeEnum.ZONE,
718                           collection=zonal_collection)
719    if regional_collection:
720      self.scopes.AddScope(compute_scope.ScopeEnum.REGION,
721                           collection=regional_collection)
722    if global_collection:
723      self.scopes.AddScope(compute_scope.ScopeEnum.GLOBAL,
724                           collection=global_collection)
725    self._global_help_text = global_help_text
726    self._region_explanation = region_explanation or ''
727    self._region_help_text = region_help_text
728    self._region_hidden = region_hidden
729    self._zone_explanation = zone_explanation or ''
730    self._zone_help_text = zone_help_text
731    self._zone_hidden = zone_hidden
732    self._resource_resolver = ResourceResolver(self.scopes, resource_name)
733
734  # TODO(b/31933786) remove cust_metavar once surface supports metavars for
735  # plural flags.
736  # TODO(b/32116723) remove mutex_group when argparse handles nesting groups
737  def AddArgument(self,
738                  parser,
739                  mutex_group=None,
740                  operation_type='operate on',
741                  cust_metavar=None):
742    """Add this set of arguments to argparse parser."""
743
744    params = dict(
745        metavar=cust_metavar if cust_metavar else self.name.upper(),
746        completer=self.completer,
747    )
748
749    if self._detailed_help:
750      params['help'] = self._detailed_help
751    elif self._short_help:
752      params['help'] = self._short_help
753    else:
754      params['help'] = 'Name{} of the {} to {}.'.format(
755          's' if self.plural else '',
756          text.Pluralize(
757              int(self.plural) + 1, self.resource_name or '',
758              self.custom_plural),
759          operation_type)
760      if self.name.startswith('instance'):
761        params['help'] += (' For details on valid instance names, refer '
762                           'to the criteria documented under the field '
763                           '\'name\' at: '
764                           'https://cloud.google.com/compute/docs/reference/'
765                           'rest/v1/instances')
766      if self.name == 'DISK_NAME' and operation_type == 'create':
767        params['help'] += (' For details on the naming convention for this '
768                           'resource, refer to: '
769                           'https://cloud.google.com/compute/docs/'
770                           'naming-resources')
771
772    if self.name_arg.startswith('--'):
773      params['required'] = self.required
774      if self.plural:
775        params['type'] = arg_parsers.ArgList(min_length=1)
776    else:
777      if self.required:
778        if self.plural:
779          params['nargs'] = '+'
780      else:
781        params['nargs'] = '*' if self.plural else '?'
782
783    (mutex_group or parser).add_argument(self.name_arg, **params)
784
785    if self.use_existing_default_scope:
786      return
787
788    if len(self.scopes) > 1:
789      scope = parser.add_mutually_exclusive_group()
790    else:
791      scope = parser
792
793    if compute_scope.ScopeEnum.ZONE in self.scopes:
794      AddZoneFlag(
795          scope,
796          flag_prefix=self.scopes.flag_prefix,
797          resource_type=self.resource_name,
798          operation_type=operation_type,
799          explanation=self._zone_explanation,
800          help_text=self._zone_help_text,
801          hidden=self._zone_hidden,
802          plural=self.plural,
803          custom_plural=self.custom_plural)
804
805    if compute_scope.ScopeEnum.REGION in self.scopes:
806      AddRegionFlag(
807          scope,
808          flag_prefix=self.scopes.flag_prefix,
809          resource_type=self.resource_name,
810          operation_type=operation_type,
811          explanation=self._region_explanation,
812          help_text=self._region_help_text,
813          hidden=self._region_hidden,
814          plural=self.plural,
815          custom_plural=self.custom_plural)
816
817    if not self.plural:
818      resource_mention = '{} is'.format(self.resource_name)
819    elif self.plural and not self.custom_plural:
820      resource_mention = '{}s are'.format(self.resource_name)
821    else:
822      resource_mention = '{} are'.format(self.custom_plural)
823    if compute_scope.ScopeEnum.GLOBAL in self.scopes and len(self.scopes) > 1:
824      scope.add_argument(
825          self.scopes[compute_scope.ScopeEnum.GLOBAL].flag,
826          action='store_true',
827          default=None,
828          help=self._global_help_text or 'If set, the {0} global.'
829          .format(resource_mention))
830
831  def ResolveAsResource(self,
832                        args,
833                        api_resource_registry,
834                        default_scope=None,
835                        scope_lister=None,
836                        with_project=True):
837    """Resolve this resource against the arguments.
838
839    Args:
840      args: Namespace, argparse.Namespace.
841      api_resource_registry: instance of core.resources.Registry.
842      default_scope: ScopeEnum, ZONE, REGION, GLOBAL, or None when resolving
843          name and scope was not specified use this as default. If there is
844          exactly one possible scope it will be used, there is no need to
845          specify default_scope.
846      scope_lister: func(scope, underspecified_names), a callback which returns
847        list of items (with 'name' attribute) for given scope.
848      with_project: indicates whether or not project is associated. It should be
849        False for flexible resource APIs.
850    Returns:
851      Resource reference or list of references if plural.
852    """
853    names = self._GetResourceNames(args)
854    resource_scope, scope_value = self.scopes.SpecifiedByArgs(args)
855    if resource_scope is not None:
856      resource_scope = resource_scope.scope_enum
857      # Complain if scope was specified without actual resource(s).
858      if not self.required and not names:
859        if self.scopes.flag_prefix:
860          flag = '--{0}-{1}'.format(
861              self.scopes.flag_prefix, resource_scope.flag_name)
862        else:
863          flag = '--' + resource_scope
864        raise exceptions.Error(
865            'Can\'t specify {0} without specifying resource via {1}'.format(
866                flag, self.name))
867    refs = self._resource_resolver.ResolveResources(
868        names,
869        resource_scope,
870        scope_value,
871        api_resource_registry,
872        default_scope,
873        scope_lister,
874        with_project=with_project)
875    if self.plural:
876      return refs
877    if refs:
878      return refs[0]
879    return None
880
881  def _GetResourceNames(self, args):
882    """Return list of resource names specified by args."""
883    if self.plural:
884      return getattr(args, self.name)
885
886    name_value = getattr(args, self.name)
887    if name_value is not None:
888      return [name_value]
889    return []
890
891
892def AddRegexArg(parser):
893  parser.add_argument(
894      '--regexp', '-r',
895      help="""\
896      A regular expression to filter the names of the results on. Any names
897      that do not match the entire regular expression will be filtered out.
898      """)
899
900
901def AddPolicyFileFlag(parser):
902  parser.add_argument('policy_file', help="""\
903      JSON or YAML file containing the IAM policy.""")
904
905
906def AddStorageLocationFlag(parser, resource):
907  parser.add_argument(
908      '--storage-location',
909      metavar='LOCATION',
910      help="""\
911      Google Cloud Storage location, either regional or multi-regional, where
912      {} content is to be stored. If absent, a nearby regional or
913      multi-regional location is chosen automatically.
914      """.format(resource))
915
916
917def AddGuestFlushFlag(parser, resource, custom_help=None):
918  help_text = """
919  Create an application-consistent {} by informing the OS
920  to prepare for the snapshot process. Currently only supported
921  on Windows instances using the Volume Shadow Copy Service (VSS).
922  """.format(resource)
923  parser.add_argument(
924      '--guest-flush',
925      action='store_true',
926      default=False,
927      help=custom_help if custom_help else help_text)
928
929
930def AddShieldedInstanceInitialStateKeyArg(parser):
931  """Adds the initial state for Shielded instance arg."""
932  parser.add_argument(
933      '--platform-key-file',
934      help="""\
935      File path that points to an X.509 certificate in DER format or raw binary
936      file. When you create a Shielded VM instance from this image, this
937      certificate or raw binary file is used as the platform key (PK).
938        """)
939  parser.add_argument(
940      '--key-exchange-key-file',
941      type=arg_parsers.ArgList(),
942      metavar='KEK_VALUE',
943      help="""\
944      Comma-separated list of file paths that point to X.509 certificates in DER
945      format or raw binary files. When you create a Shielded VM instance from
946      this image, these certificates or files are used as key exchange keys
947      (KEK).
948        """)
949  parser.add_argument(
950      '--signature-database-file',
951      type=arg_parsers.ArgList(),
952      metavar='DB_VALUE',
953      help="""\
954      Comma-separated list of file paths that point to valid X.509 certificates
955      in DER format or raw binary files. When you create a Shielded VM instance
956      from this image, these certificates or files are  added to the signature
957      database (db).
958        """)
959  parser.add_argument(
960      '--forbidden-database-file',
961      type=arg_parsers.ArgList(),
962      metavar='DBX_VALUE',
963      help="""\
964      Comma-separated list of file paths that point to revoked X.509
965      certificates in DER format or raw binary files. When you create a Shielded
966      VM instance from this image, these certificates or files are added to the
967      forbidden signature database (dbx).
968        """)
969
970
971def RewriteFilter(args, message=None, frontend_fields=None):
972  """Rewrites args.filter into client and server filter expression strings.
973
974  Usage:
975
976    args.filter, request_filter = flags.RewriteFilter(args)
977
978  Args:
979    args: The parsed args namespace containing the filter expression args.filter
980      and display_info.
981    message: The response resource message proto for the request.
982    frontend_fields: A set of dotted key names supported client side only.
983
984  Returns:
985    A (client_filter, server_filter) tuple of filter expression strings.
986    None means the filter does not need to applied on the respective
987    client/server side.
988  """
989  if not args.filter:
990    return None, None
991  display_info = args.GetDisplayInfo()
992  defaults = resource_projection_spec.ProjectionSpec(
993      symbols=display_info.transforms,
994      aliases=display_info.aliases)
995  client_filter, server_filter = filter_rewrite.Rewriter(
996      message=message, frontend_fields=frontend_fields).Rewrite(
997          args.filter, defaults=defaults)
998  log.info('client_filter=%r server_filter=%r', client_filter, server_filter)
999  return client_filter, server_filter
1000
1001
1002def AddSourceDiskCsekKeyArg(parser):
1003  spec = {
1004      'disk': str,
1005      'csek-key-file': str
1006  }
1007  parser.add_argument(
1008      '--source-disk-csek-key',
1009      type=arg_parsers.ArgDict(spec=spec),
1010      action='append',
1011      metavar='PROPERTY=VALUE',
1012      help="""
1013              Customer-supplied encryption key of the disk attached to the
1014              source instance. Required if the source disk is protected by
1015              a customer-supplied encryption key. This flag may be repeated to
1016              specify multiple attached disks.
1017
1018              *disk*::: URL of the disk attached to the source instance.
1019              This can be a full or   valid partial URL
1020
1021              *csek-key-file*::: path to customer-supplied encryption key.
1022            """
1023  )
1024
1025
1026def AddEraseVssSignature(parser, resource):
1027  parser.add_argument(
1028      '--erase-windows-vss-signature',
1029      action='store_true',
1030      default=False,
1031      help="""
1032              Specifies whether the disk restored from {resource} should
1033              erase Windows specific VSS signature.
1034              See https://cloud.google.com/sdk/gcloud/reference/compute/disks/snapshot#--guest-flush
1035           """.format(resource=resource)
1036  )
1037