# -*- coding: utf-8 -*- # # Copyright 2019 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Flags and helpers for the compute related commands.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import copy import enum import functools import re from googlecloudsdk.api_lib.compute import filter_rewrite from googlecloudsdk.api_lib.compute.regions import service as regions_service from googlecloudsdk.api_lib.compute.zones import service as zones_service from googlecloudsdk.calliope import actions from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.command_lib.compute import completers from googlecloudsdk.command_lib.compute import scope as compute_scope from googlecloudsdk.command_lib.compute import scope_prompter from googlecloudsdk.core import exceptions from googlecloudsdk.core import log from googlecloudsdk.core import properties from googlecloudsdk.core import resources from googlecloudsdk.core.console import console_io from googlecloudsdk.core.resource import resource_projection_spec from googlecloudsdk.core.util import text import six _GLOBAL_RELATIVE_PATH_REGEX = 'projects/([^/]+)/global/([^/]+)/' _REGIONAL_RELATIVE_PATH_REGEX = 'projects/([^/]+)/regions/([^/]+)/' _ZONAL_RELATIVE_PATH_REGEX = 'projects/([^/]+)/zones/([^/]+)/' ZONE_PROPERTY_EXPLANATION = """\ If not specified and the ``compute/zone'' property isn't set, you might be prompted to select a zone (interactive mode only). To avoid prompting when this flag is omitted, you can set the ``compute/zone'' property: $ gcloud config set compute/zone ZONE A list of zones can be fetched by running: $ gcloud compute zones list To unset the property, run: $ gcloud config unset compute/zone Alternatively, the zone can be stored in the environment variable ``CLOUDSDK_COMPUTE_ZONE''. """ ZONE_PROPERTY_EXPLANATION_NO_DEFAULT = """\ If not specified, you might be prompted to select a zone (interactive mode only). A list of zones can be fetched by running: $ gcloud compute zones list """ REGION_PROPERTY_EXPLANATION = """\ If not specified, you might be prompted to select a region (interactive mode only). To avoid prompting when this flag is omitted, you can set the ``compute/region'' property: $ gcloud config set compute/region REGION A list of regions can be fetched by running: $ gcloud compute regions list To unset the property, run: $ gcloud config unset compute/region Alternatively, the region can be stored in the environment variable ``CLOUDSDK_COMPUTE_REGION''. """ REGION_PROPERTY_EXPLANATION_NO_DEFAULT = """\ If not specified, you might be prompted to select a region (interactive mode only). A list of regions can be fetched by running: $ gcloud compute regions list """ class ScopeFlagsUsage(enum.Enum): """Enum representing gcloud flag generation options for ResourceArgument.""" GENERATE_DEDICATED_SCOPE_FLAGS = 1 USE_EXISTING_SCOPE_FLAGS = 2 DONT_USE_SCOPE_FLAGS = 3 class ScopesFetchingException(exceptions.Error): pass class BadArgumentException(ValueError): """Unhandled error for validating function arguments.""" pass def AddZoneFlag(parser, resource_type, operation_type, flag_prefix=None, explanation=ZONE_PROPERTY_EXPLANATION, help_text=None, hidden=False, plural=False, custom_plural=None): """Adds a --zone flag to the given parser. Args: parser: argparse parser. resource_type: str, human readable name for the resource type this flag is qualifying, for example "instance group". operation_type: str, human readable name for the operation, for example "update" or "delete". flag_prefix: str, flag will be named --{flag_prefix}-zone. explanation: str, detailed explanation of the flag. help_text: str, help text will be overridden with this value. hidden: bool, If True, --zone argument help will be hidden. plural: bool, resource_type will be pluralized or not depending on value. custom_plural: str, If plural is True then this string will be used as resource types, otherwise resource_types will be pluralized by appending 's'. """ short_help = 'Zone of the {0} to {1}.'.format( text.Pluralize( int(plural) + 1, resource_type or '', custom_plural), operation_type) flag_name = 'zone' if flag_prefix is not None: flag_name = flag_prefix + '-' + flag_name parser.add_argument( '--' + flag_name, hidden=hidden, completer=completers.ZonesCompleter, action=actions.StoreProperty(properties.VALUES.compute.zone), help=help_text or '{0} {1}'.format(short_help, explanation)) def AddRegionFlag(parser, resource_type, operation_type, flag_prefix=None, explanation=REGION_PROPERTY_EXPLANATION, help_text=None, hidden=False, plural=False, custom_plural=None): """Adds a --region flag to the given parser. Args: parser: argparse parser. resource_type: str, human readable name for the resource type this flag is qualifying, for example "instance group". operation_type: str, human readable name for the operation, for example "update" or "delete". flag_prefix: str, flag will be named --{flag_prefix}-region. explanation: str, detailed explanation of the flag. help_text: str, help text will be overridden with this value. hidden: bool, If True, --region argument help will be hidden. plural: bool, resource_type will be pluralized or not depending on value. custom_plural: str, If plural is True then this string will be used as resource types, otherwise resource_types will be pluralized by appending 's'. """ short_help = 'Region of the {0} to {1}.'.format( text.Pluralize( int(plural) + 1, resource_type or '', custom_plural), operation_type) flag_name = 'region' if flag_prefix is not None: flag_name = flag_prefix + '-' + flag_name parser.add_argument( '--' + flag_name, completer=completers.RegionsCompleter, action=actions.StoreProperty(properties.VALUES.compute.region), hidden=hidden, help=help_text or '{0} {1}'.format(short_help, explanation)) class UnderSpecifiedResourceError(exceptions.Error): """Raised when argument is required additional scope to be resolved.""" def __init__(self, underspecified_names, flag_names): phrases = ('one of ', 'flags') if len(flag_names) > 1 else ('', 'flag') super(UnderSpecifiedResourceError, self).__init__( 'Underspecified resource [{3}]. Specify {0}the [{1}] {2}.' .format(phrases[0], ', '.join(sorted(flag_names)), phrases[1], ', '.join(underspecified_names))) class ResourceStub(object): """Interface used by scope listing to report scope names.""" def __init__(self, name, deprecated=None): self.name = name self.deprecated = deprecated def GetDefaultScopeLister(compute_client, project=None): """Constructs default zone/region lister.""" scope_func = { compute_scope.ScopeEnum.ZONE: functools.partial(zones_service.List, compute_client), compute_scope.ScopeEnum.REGION: functools.partial(regions_service.List, compute_client), compute_scope.ScopeEnum.GLOBAL: lambda _: [ResourceStub(name='')] } def Lister(scopes, _): prj = project or properties.VALUES.core.project.Get(required=True) results = {} for scope in scopes: results[scope] = scope_func[scope](prj) return results return Lister class ResourceArgScope(object): """Facilitates mapping of scope, flag and collection.""" def __init__(self, scope, flag_prefix, collection): self.scope_enum = scope if flag_prefix: flag_prefix = flag_prefix.replace('-', '_') if scope is compute_scope.ScopeEnum.GLOBAL: self.flag_name = scope.flag_name + '_' + flag_prefix else: self.flag_name = flag_prefix + '_' + scope.flag_name else: self.flag_name = scope.flag_name self.flag = '--' + self.flag_name.replace('_', '-') self.collection = collection class ResourceArgScopes(object): """Represents chosen set of scopes.""" def __init__(self, flag_prefix): self.flag_prefix = flag_prefix self.scopes = {} def AddScope(self, scope, collection): self.scopes[scope] = ResourceArgScope(scope, self.flag_prefix, collection) def SpecifiedByArgs(self, args): """Given argparse args return selected scope and its value.""" for resource_scope in six.itervalues(self.scopes): scope_value = getattr(args, resource_scope.flag_name, None) if scope_value is not None: return resource_scope, scope_value return None, None def SpecifiedByValue(self, value): """Given resource value return selected scope and its value.""" if re.match(_GLOBAL_RELATIVE_PATH_REGEX, value): return self.scopes[compute_scope.ScopeEnum.GLOBAL], 'global' elif re.match(_REGIONAL_RELATIVE_PATH_REGEX, value): return self.scopes[compute_scope.ScopeEnum.REGION], 'region' elif re.match(_ZONAL_RELATIVE_PATH_REGEX, value): return self.scopes[compute_scope.ScopeEnum.ZONE], 'zone' return None, None def GetImplicitScope(self, default_scope=None): """See if there is no ambiguity even if scope is not known from args.""" if len(self.scopes) == 1: return next(six.itervalues(self.scopes)) return default_scope def __iter__(self): return iter(six.itervalues(self.scopes)) def __contains__(self, scope): return scope in self.scopes def __getitem__(self, scope): return self.scopes[scope] def __len__(self): return len(self.scopes) class ResourceResolver(object): """Object responsible for resolving resources. There are two ways to build an instance of this object: 1. Preferred when you don't have instance of ResourceArgScopes already built, using .FromMap static function. For example: resolver = ResourceResolver.FromMap( 'instance', {compute_scope.ScopeEnum.ZONE: 'compute.instances'}) where: - 'instance' is human readable name of the resource, - dictionary maps allowed scope (in this case only zone) to resource types in those scopes. - optional prefix of scope flags was skipped. 2. Using constructor. Recommended only if you have instance of ResourceArgScopes available. Once you've built the resover you can use it to build resource references (and prompt for scope if it was not specified): resolver.ResolveResources( instance_name, compute_scope.ScopeEnum.ZONE, instance_zone, self.resources, scope_lister=flags.GetDefaultScopeLister( self.compute_client, self.project)) will return a list of instances (of length 0 or 1 in this case, because we pass a name of single instance or None). It will prompt if and only if instance_name was not None but instance_zone was None. scope_lister is necessary for prompting. """ def __init__(self, scopes, resource_name): """Initilize ResourceResolver instance. Prefer building with FromMap unless you have ResourceArgScopes object already built. Args: scopes: ResourceArgScopes, allowed scopes and resource types in those scopes. resource_name: str, human readable name for resources eg "instance group". """ self.scopes = scopes self.resource_name = resource_name @staticmethod def FromMap(resource_name, scopes_map, scope_flag_prefix=None): """Initilize ResourceResolver instance. Args: resource_name: str, human readable name for resources eg "instance group". scopes_map: dict, with keys should be instances of ScopeEnum, values should be instances of ResourceArgScope. scope_flag_prefix: str, prefix of flags specyfying scope. Returns: New instance of ResourceResolver. """ scopes = ResourceArgScopes(flag_prefix=scope_flag_prefix) for scope, resource in six.iteritems(scopes_map): scopes.AddScope(scope, resource) return ResourceResolver(scopes, resource_name) def _ValidateNames(self, names): if not isinstance(names, list): raise BadArgumentException( "Expected names to be a list but it is '{0}'".format(names)) def _ValidateDefaultScope(self, default_scope): if default_scope is not None and default_scope not in self.scopes: raise BadArgumentException( 'Unexpected value for default_scope {0}, expected None or {1}' .format(default_scope, ' or '.join([s.scope_enum.name for s in self.scopes]))) def _GetResourceScopeParam(self, resource_scope, scope_value, project, api_resource_registry, with_project=True): """Gets the resource scope parameters.""" if scope_value is not None: if resource_scope.scope_enum == compute_scope.ScopeEnum.GLOBAL: return None else: collection = compute_scope.ScopeEnum.CollectionForScope( resource_scope.scope_enum) if with_project: return api_resource_registry.Parse( scope_value, params={ 'project': project }, collection=collection).Name() else: return api_resource_registry.Parse( scope_value, params={}, collection=collection).Name() else: if resource_scope and (resource_scope.scope_enum != compute_scope.ScopeEnum.GLOBAL): return resource_scope.scope_enum.property_func def _GetRefsAndUnderspecifiedNames( self, names, params, collection, scope_defined, api_resource_registry): """Returns pair of lists: resolved references and unresolved names. Args: names: list of names to attempt resolving params: params given when attempting to resolve references collection: collection for the names scope_defined: bool, whether scope is known api_resource_registry: Registry object """ refs = [] underspecified_names = [] for name in names: try: # Make each element an array so that we can do in place updates. ref = [api_resource_registry.Parse(name, params=params, collection=collection, enforce_collection=False)] except (resources.UnknownCollectionException, resources.RequiredFieldOmittedException, properties.RequiredPropertyError): if scope_defined: raise ref = [name] underspecified_names.append(ref) refs.append(ref) return refs, underspecified_names def _ResolveMultiScope(self, with_project, project, underspecified_names, api_resource_registry, refs): """Resolve argument against available scopes of the resource.""" names = copy.deepcopy(underspecified_names) for scope in self.scopes: if with_project: params = { 'project': project, } else: params = {} params[scope.scope_enum.param_name] = scope.scope_enum.property_func for name in names: try: ref = [api_resource_registry.Parse(name[0], params=params, collection=scope.collection, enforce_collection=False)] refs.remove(name) refs.append(ref) underspecified_names.remove(name) except (resources.UnknownCollectionException, resources.RequiredFieldOmittedException, properties.RequiredPropertyError, ValueError): continue def _ResolveUnderspecifiedNames(self, underspecified_names, default_scope, scope_lister, project, api_resource_registry, with_project=True): """Attempt to resolve scope for unresolved names. If unresolved_names was generated with _GetRefsAndUnderspecifiedNames changing them will change corresponding elements of refs list. Args: underspecified_names: list of one-items lists containing str default_scope: default scope for the resources scope_lister: callback used to list potential scopes for the resources project: str, id of the project api_resource_registry: resources Registry with_project: indicates whether or not project is associated. It should be False for flexible resource APIs Raises: UnderSpecifiedResourceError: when resource scope can't be resolved. """ if not underspecified_names: return names = [n[0] for n in underspecified_names] if not console_io.CanPrompt(): raise UnderSpecifiedResourceError(names, [s.flag for s in self.scopes]) resource_scope_enum, scope_value = scope_prompter.PromptForScope( self.resource_name, names, [s.scope_enum for s in self.scopes], default_scope.scope_enum if default_scope is not None else None, scope_lister) if resource_scope_enum is None: raise UnderSpecifiedResourceError(names, [s.flag for s in self.scopes]) resource_scope = self.scopes[resource_scope_enum] if with_project: params = { 'project': project, } else: params = {} if resource_scope.scope_enum != compute_scope.ScopeEnum.GLOBAL: params[resource_scope.scope_enum.param_name] = scope_value for name in underspecified_names: name[0] = api_resource_registry.Parse( name[0], params=params, collection=resource_scope.collection, enforce_collection=True) def ResolveResources(self, names, resource_scope, scope_value, api_resource_registry, default_scope=None, scope_lister=None, with_project=True, source_project=None, additional_params=None): """Resolve this resource against the arguments. Args: names: list of str, list of resource names resource_scope: ScopeEnum, kind of scope of resources; if this is not None scope_value should be name of scope of type specified by this argument. If this is None scope_value should be None, in that case if prompting is possible user will be prompted to select scope (if prompting is forbidden it will raise an exception). scope_value: ScopeEnum, scope of resources; if this is not None resource_scope should be type of scope specified by this argument. If this is None resource_scope should be None, in that case if prompting is possible user will be prompted to select scope (if prompting is forbidden it will raise an exception). api_resource_registry: instance of core.resources.Registry. default_scope: ScopeEnum, ZONE, REGION, GLOBAL, or None when resolving name and scope was not specified use this as default. If there is exactly one possible scope it will be used, there is no need to specify default_scope. scope_lister: func(scope, underspecified_names), a callback which returns list of items (with 'name' attribute) for given scope. with_project: indicates whether or not project is associated. It should be False for flexible resource APIs. source_project: indicates whether or not a project is specified. It could be other projects. If it is None, then it will use the current project if with_project is true additional_params: Dict, additional parameters to pass in. Returns: Resource reference or list of references if plural. Raises: BadArgumentException: when names is not a list or default_scope is not one of the configured scopes. UnderSpecifiedResourceError: if it was not possible to resolve given names as resources references. """ self._ValidateNames(names) self._ValidateDefaultScope(default_scope) if resource_scope is not None: resource_scope = self.scopes[resource_scope] if default_scope is not None: default_scope = self.scopes[default_scope] if source_project is not None: source_project_ref = api_resource_registry.Parse( source_project, collection='compute.projects') source_project = source_project_ref.Name() project = None if with_project: project = source_project or properties.VALUES.core.project.GetOrFail() params = { 'project': project, } else: params = {} if scope_value is None: resource_scope = self.scopes.GetImplicitScope(default_scope) if additional_params: params = {**params, **additional_params} resource_scope_param = self._GetResourceScopeParam( resource_scope, scope_value, project, api_resource_registry, with_project=with_project) if resource_scope_param is not None: params[resource_scope.scope_enum.param_name] = resource_scope_param collection = resource_scope and resource_scope.collection # See if we can resolve names with so far deduced scope and its value. refs, underspecified_names = self._GetRefsAndUnderspecifiedNames( names, params, collection, scope_value is not None, api_resource_registry) # Try to resolve with each available scope if underspecified_names and len(self.scopes) > 1: self._ResolveMultiScope(with_project, project, underspecified_names, api_resource_registry, refs) # If we still have some resources which need to be resolve see if we can # prompt the user and try to resolve these again. self._ResolveUnderspecifiedNames( underspecified_names, default_scope, scope_lister, project, api_resource_registry, with_project=with_project) # Now unpack each element. refs = [ref[0] for ref in refs] # Make sure correct collection was given for each resource, for example # URLs have implicit collections. expected_collections = [scope.collection for scope in self.scopes] for ref in refs: if ref.Collection() not in expected_collections: raise resources.WrongResourceCollectionException( expected=','.join(expected_collections), got=ref.Collection(), path=ref.SelfLink()) return refs class ResourceArgument(object): """Encapsulates concept of compute resource as command line argument. Basic Usage: class MyCommand(base.Command): _BACKEND_SERVICE_ARG = flags.ResourceArgument( resource_name='backend service', completer=compute_completers.BackendServiceCompleter, regional_collection='compute.regionBackendServices', global_collection='compute.backendServices') _INSTANCE_GROUP_ARG = flags.ResourceArgument( resource_name='instance group', completer=compute_completers.InstanceGroupsCompleter, zonal_collection='compute.instanceGroups',) @staticmethod def Args(parser): MyCommand._BACKEND_SERVICE_ARG.AddArgument(parser) MyCommand._INSTANCE_GROUP_ARG.AddArgument(parser) def Run(args): api_resource_registry = resources.REGISTRY.CloneAndSwitch( api_tools_client) backend_service_ref = _BACKEND_SERVICE_ARG.ResolveAsResource( args, api_resource_registry, default_scope=flags.ScopeEnum.GLOBAL) instance_group_ref = _INSTANCE_GROUP_ARG.ResolveAsResource( args, api_resource_registry, default_scope=flags.ScopeEnum.ZONE) ... In the above example the following five arguments/flags will be defined: NAME - positional for backend service --region REGION to qualify backend service --global to qualify backend service --instance-group INSTANCE_GROUP name for the instance group --instance-group-zone INSTANCE_GROUP_ZONE further qualifies instance group More generally this construct can simultaneously support global, regional and zonal qualifiers (or any combination of) for each resource. """ def __init__( self, name=None, resource_name=None, completer=None, plural=False, required=True, zonal_collection=None, regional_collection=None, global_collection=None, global_help_text=None, region_explanation=None, region_help_text=None, region_hidden=False, zone_explanation=None, zone_help_text=None, zone_hidden=False, short_help=None, detailed_help=None, custom_plural=None, scope_flags_usage=ScopeFlagsUsage.GENERATE_DEDICATED_SCOPE_FLAGS): """Constructor. Args: name: str, argument name. resource_name: str, human readable name for resources eg "instance group". completer: completion_cache.Completer, The completer class type. plural: bool, whether to accept multiple values. required: bool, whether this argument is required. zonal_collection: str, include zone flag and use this collection to resolve it. regional_collection: str, include region flag and use this collection to resolve it. global_collection: str, if also zonal and/or regional adds global flag and uses this collection to resolve as global resource. global_help_text: str, if provided, global flag help text will be overridden with this value. region_explanation: str, long help that will be given for region flag, empty by default. region_help_text: str, if provided, region flag help text will be overridden with this value. region_hidden: bool, Hide region in help if True. zone_explanation: str, long help that will be given for zone flag, empty by default. zone_help_text: str, if provided, zone flag help text will be overridden with this value. zone_hidden: bool, Hide zone in help if True. short_help: str, help for the flag being added, if not provided help text will be 'The name[s] of the ${resource_name}[s].'. detailed_help: str, detailed help for the flag being added, if not provided there will be no detailed help for the flag. custom_plural: str, If plural is True then this string will be used as plural resource name. scope_flags_usage: ScopeFlagsUsage, when set to USE_EXISTING_SCOPE_FLAGS, already existing zone and/or region flags will be used for this argument, GENERATE_DEDICATED_SCOPE_FLAGS, new scope flags will be created, DONT_USE_SCOPE_FLAGS to not generate additional flags and use single argument for all scopes. Raises: exceptions.Error: if there some inconsistency in arguments. """ self.name_arg = name or 'name' self._short_help = short_help self._detailed_help = detailed_help self.scope_flags_usage = scope_flags_usage if self.name_arg.startswith('--'): self.is_flag = True self.name = self.name_arg[2:].replace('-', '_') flag_prefix = (None if self.scope_flags_usage == ScopeFlagsUsage.USE_EXISTING_SCOPE_FLAGS else self.name_arg[2:]) self.scopes = ResourceArgScopes(flag_prefix=flag_prefix) else: # positional self.scopes = ResourceArgScopes(flag_prefix=None) self.name = self.name_arg # arg name is same as its spec. self.resource_name = resource_name self.completer = completer self.plural = plural self.custom_plural = custom_plural self.required = required if not (zonal_collection or regional_collection or global_collection): raise exceptions.Error('Must specify at least one resource type zonal, ' 'regional or global') if zonal_collection: self.scopes.AddScope(compute_scope.ScopeEnum.ZONE, collection=zonal_collection) if regional_collection: self.scopes.AddScope(compute_scope.ScopeEnum.REGION, collection=regional_collection) if global_collection: self.scopes.AddScope(compute_scope.ScopeEnum.GLOBAL, collection=global_collection) self._global_help_text = global_help_text self._region_explanation = region_explanation or '' self._region_help_text = region_help_text self._region_hidden = region_hidden self._zone_explanation = zone_explanation or '' self._zone_help_text = zone_help_text self._zone_hidden = zone_hidden self._resource_resolver = ResourceResolver(self.scopes, resource_name) # TODO(b/31933786) remove cust_metavar once surface supports metavars for # plural flags. def AddArgument( self, parser, mutex_group=None, operation_type='operate on', cust_metavar=None, category=None, scope_required=False, ): """Add this set of arguments to argparse parser.""" params = dict( metavar=cust_metavar if cust_metavar else self.name.upper(), completer=self.completer, ) if self._detailed_help: params['help'] = self._detailed_help elif self._short_help: params['help'] = self._short_help else: params['help'] = 'Name{} of the {} to {}.'.format( 's' if self.plural else '', text.Pluralize( int(self.plural) + 1, self.resource_name or '', self.custom_plural), operation_type) if self.name.startswith('instance'): params['help'] += (' For details on valid instance names, refer ' 'to the criteria documented under the field ' '\'name\' at: ' 'https://cloud.google.com/compute/docs/reference/' 'rest/v1/instances') if self.name == 'DISK_NAME' and operation_type == 'create': params['help'] += (' For details on the naming convention for this ' 'resource, refer to: ' 'https://cloud.google.com/compute/docs/' 'naming-resources') if self.name_arg.startswith('--'): params['required'] = self.required if not self.required: # Only not required flags can be group by category. params['category'] = category if self.plural: params['type'] = arg_parsers.ArgList(min_length=1) else: if self.required: if self.plural: params['nargs'] = '+' else: params['nargs'] = '*' if self.plural else '?' (mutex_group or parser).add_argument(self.name_arg, **params) if self.scope_flags_usage != ScopeFlagsUsage.GENERATE_DEDICATED_SCOPE_FLAGS: return if len(self.scopes) > 1: scope = parser.add_group( mutex=True, category=category, required=scope_required ) else: scope = parser if compute_scope.ScopeEnum.ZONE in self.scopes: AddZoneFlag( scope, flag_prefix=self.scopes.flag_prefix, resource_type=self.resource_name, operation_type=operation_type, explanation=self._zone_explanation, help_text=self._zone_help_text, hidden=self._zone_hidden, plural=self.plural, custom_plural=self.custom_plural) if compute_scope.ScopeEnum.REGION in self.scopes: AddRegionFlag( scope, flag_prefix=self.scopes.flag_prefix, resource_type=self.resource_name, operation_type=operation_type, explanation=self._region_explanation, help_text=self._region_help_text, hidden=self._region_hidden, plural=self.plural, custom_plural=self.custom_plural) if compute_scope.ScopeEnum.GLOBAL in self.scopes and len(self.scopes) > 1: if not self.plural: resource_mention = '{} is'.format(self.resource_name) elif self.plural and not self.custom_plural: resource_mention = '{}s are'.format(self.resource_name) else: resource_mention = '{} are'.format(self.custom_plural) scope.add_argument( self.scopes[compute_scope.ScopeEnum.GLOBAL].flag, action='store_true', default=None, help=self._global_help_text or 'If set, the {0} global.' .format(resource_mention)) def ResolveAsResource(self, args, api_resource_registry, default_scope=None, scope_lister=None, with_project=True, source_project=None, additional_params=None): """Resolve this resource against the arguments. Args: args: Namespace, argparse.Namespace. api_resource_registry: instance of core.resources.Registry. default_scope: ScopeEnum, ZONE, REGION, GLOBAL, or None when resolving name and scope was not specified use this as default. If there is exactly one possible scope it will be used, there is no need to specify default_scope. scope_lister: func(scope, underspecified_names), a callback which returns list of items (with 'name' attribute) for given scope. with_project: indicates whether or not project is associated. It should be False for flexible resource APIs. source_project: indicates whether or not a project is specified. It could be other projects. If it is None, then it will use the current project if with_project is true additional_params: Dict, additional parameters to pass in. Returns: Resource reference or list of references if plural. """ names = self._GetResourceNames(args) resource_scope, scope_value = self.scopes.SpecifiedByArgs(args) if ( resource_scope is None and self.scope_flags_usage == ScopeFlagsUsage.DONT_USE_SCOPE_FLAGS ): resource_scope, scope_value = self.scopes.SpecifiedByValue(names[0]) if resource_scope is not None: resource_scope = resource_scope.scope_enum # Complain if scope was specified without actual resource(s). if not self.required and not names: if self.scopes.flag_prefix: flag = '--{0}-{1}'.format( self.scopes.flag_prefix, resource_scope.flag_name) else: flag = '--' + resource_scope raise exceptions.Error( 'Can\'t specify {0} without specifying resource via {1}'.format( flag, self.name)) refs = self._resource_resolver.ResolveResources( names, resource_scope, scope_value, api_resource_registry, default_scope, scope_lister, with_project=with_project, source_project=source_project, additional_params=additional_params) if self.plural: return refs if refs: return refs[0] return None def _GetResourceNames(self, args): """Return list of resource names specified by args.""" if self.plural: return getattr(args, self.name) name_value = getattr(args, self.name) if name_value is not None: return [name_value] return [] def AddRegexArg(parser): parser.add_argument( '--regexp', '-r', help="""\ A regular expression to filter the names of the results on. Any names that do not match the entire regular expression will be filtered out. """) def AddPolicyFileFlag(parser): parser.add_argument('policy_file', help="""\ JSON or YAML file containing the IAM policy.""") def AddStorageLocationFlag(parser, resource): parser.add_argument( '--storage-location', metavar='LOCATION', help="""\ Google Cloud Storage location, either regional or multi-regional, where {} content is to be stored. If absent, a nearby regional or multi-regional location is chosen automatically. """.format(resource)) def AddGuestFlushFlag(parser, resource, custom_help=None): help_text = """ Create an application-consistent {} by informing the OS to prepare for the snapshot process. """.format(resource) parser.add_argument( '--guest-flush', action='store_true', default=False, help=custom_help if custom_help else help_text) def AddShieldedInstanceInitialStateKeyArg(parser): """Adds the initial state for Shielded instance arg.""" parser.add_argument( '--platform-key-file', help="""\ File path that points to an X.509 certificate in DER format or raw binary file. When you create a Shielded VM instance from this image, this certificate or raw binary file is used as the platform key (PK). """) parser.add_argument( '--key-exchange-key-file', type=arg_parsers.ArgList(), metavar='KEK_VALUE', help="""\ Comma-separated list of file paths that point to X.509 certificates in DER format or raw binary files. When you create a Shielded VM instance from this image, these certificates or files are used as key exchange keys (KEK). """) parser.add_argument( '--signature-database-file', type=arg_parsers.ArgList(), metavar='DB_VALUE', help="""\ Comma-separated list of file paths that point to valid X.509 certificates in DER format or raw binary files. When you create a Shielded VM instance from this image, these certificates or files are added to the signature database (db). """) parser.add_argument( '--forbidden-database-file', type=arg_parsers.ArgList(), metavar='DBX_VALUE', help="""\ Comma-separated list of file paths that point to revoked X.509 certificates in DER format or raw binary files. When you create a Shielded VM instance from this image, these certificates or files are added to the forbidden signature database (dbx). """) def RewriteFilter(args, message=None, frontend_fields=None): """Rewrites args.filter into client and server filter expression strings. Usage: args.filter, request_filter = flags.RewriteFilter(args) Args: args: The parsed args namespace containing the filter expression args.filter and display_info. message: The response resource message proto for the request. frontend_fields: A set of dotted key names supported client side only. Returns: A (client_filter, server_filter) tuple of filter expression strings. None means the filter does not need to applied on the respective client/server side. """ if not args.filter: return None, None display_info = args.GetDisplayInfo() defaults = resource_projection_spec.ProjectionSpec( symbols=display_info.transforms, aliases=display_info.aliases) client_filter, server_filter = filter_rewrite.Rewriter( message=message, frontend_fields=frontend_fields).Rewrite( args.filter, defaults=defaults) log.info('client_filter=%r server_filter=%r', client_filter, server_filter) return client_filter, server_filter def AddSourceDiskCsekKeyArg(parser): spec = { 'disk': str, 'csek-key-file': str } parser.add_argument( '--source-disk-csek-key', type=arg_parsers.ArgDict(spec=spec), action='append', metavar='PROPERTY=VALUE', help=""" Customer-supplied encryption key of the disk attached to the source instance. Required if the source disk is protected by a customer-supplied encryption key. This flag can be repeated to specify multiple attached disks. *disk*::: URL of the disk attached to the source instance. This can be a full or valid partial URL *csek-key-file*::: path to customer-supplied encryption key. """ ) def AddEraseVssSignature(parser, resource): parser.add_argument( '--erase-windows-vss-signature', action='store_true', default=False, help=""" Specifies whether the disk restored from {resource} should erase Windows specific VSS signature. See https://cloud.google.com/sdk/gcloud/reference/compute/disks/snapshot#--guest-flush """.format(resource=resource) )