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