# -*- coding: utf-8 -*- # # Copyright 2014 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. """Base classes for abstracting away common logic.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import abc import argparse # pylint: disable=unused-import import json import textwrap from apitools.base.py import base_api # pylint: disable=unused-import import enum from googlecloudsdk.api_lib.compute import base_classes_resource_registry as resource_registry from googlecloudsdk.api_lib.compute import client_adapter from googlecloudsdk.api_lib.compute import constants from googlecloudsdk.api_lib.compute import lister from googlecloudsdk.api_lib.compute import property_selector from googlecloudsdk.api_lib.compute import request_helper from googlecloudsdk.api_lib.compute import resource_specs from googlecloudsdk.api_lib.compute import scope_prompter from googlecloudsdk.api_lib.compute import utils from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.compute import completers from googlecloudsdk.command_lib.compute import exceptions as compute_exceptions from googlecloudsdk.core import properties from googlecloudsdk.core import resources from googlecloudsdk.core import yaml from googlecloudsdk.core.util import text import six class ComputeApiHolder(object): """Convenience class to hold lazy initialized client and resources.""" def __init__(self, release_track, no_http=False): if release_track == base.ReleaseTrack.ALPHA: self._api_version = 'alpha' elif release_track == base.ReleaseTrack.BETA: self._api_version = 'beta' else: self._api_version = 'v1' self._client = None self._resources = None self._no_http = no_http @property def client(self): """Specifies the compute client.""" if self._client is None: self._client = client_adapter.ClientAdapter( self._api_version, self._no_http) return self._client @property def resources(self): """Specifies the resources parser for compute resources.""" if self._resources is None: self._resources = resources.REGISTRY.Clone() self._resources.RegisterApiByName('compute', self._api_version) return self._resources class BaseCommand(base.Command, scope_prompter.ScopePrompter): """Base class for all compute subcommands.""" def __init__(self, *args, **kwargs): super(BaseCommand, self).__init__(*args, **kwargs) self.__resource_spec = None self._project = properties.VALUES.core.project.Get(required=True) self._compute_holder = ComputeApiHolder(self.ReleaseTrack()) @property def _resource_spec(self): if not self.resource_type: return None if self.__resource_spec is None: # Constructing the spec can be potentially expensive (e.g., # generating the set of valid fields from the protobuf message), self.__resource_spec = resource_specs.GetSpec( self.resource_type, self.messages, self.compute_client.api_version) return self.__resource_spec @property def transformations(self): if self._resource_spec: return self._resource_spec.transformations else: return None @property def resource_type(self): """Specifies the name of the collection that should be printed.""" return None @property def http(self): """Specifies the http client to be used for requests.""" return self.compute_client.apitools_client.http @property def project(self): """Specifies the user's project.""" return self._project @property def batch_url(self): """Specifies the API batch URL.""" return self.compute_client.batch_url @property def compute_client(self): """Specifies the compute client.""" return self._compute_holder.client @property def compute(self): """Specifies the compute client.""" return self.compute_client.apitools_client @property def resources(self): """Specifies the resources parser for compute resources.""" return self._compute_holder.resources @property def messages(self): """Specifies the API message classes.""" return self.compute_client.messages def Collection(self): """Returns the resource collection path.""" return 'compute.' + self.resource_type if self.resource_type else None class BaseLister(base.ListCommand, BaseCommand): """Base class for the list subcommands.""" self_links = None names = None resource_refs = None service = None @staticmethod def Args(parser): parser.add_argument( 'names', metavar='NAME', nargs='*', default=[], completer=completers.InstancesCompleter, help=('If provided, show details for the specified names and/or URIs ' 'of resources.')) parser.add_argument( '--regexp', '-r', help="""\ Regular expression to filter the names of the results on. Any names that do not match the entire regular expression will be filtered out. """) @property def allowed_filtering_types(self): """The list of resource types that can be provided to filtering.""" return [self.resource_type] @abc.abstractmethod def GetResources(self, args, errors): """Returns a generator of JSON-serializable resource dicts.""" def GetFilterExpr(self, args): """Returns a filter expression if --regexp is provided.""" if args.regexp: return 'name eq {0}'.format(args.regexp) else: return None def PopulateResourceFilteringStructures(self, args): """Processes the positional arguments for later filtering.""" allowed_collections = ['compute.{0}'.format(resource_type) for resource_type in self.allowed_filtering_types] for name in args.names: try: ref = self.resources.Parse(name) if ref.Collection() not in allowed_collections: raise compute_exceptions.InvalidResourceError( 'Resource URI must be of type {0}. Received [{1}].'.format( ' or '.join('[{0}]'.format(collection) for collection in allowed_collections), ref.Collection())) self.self_links.add(ref.SelfLink()) self.resource_refs.append(ref) continue except resources.UserError: pass self.names.add(name) def FilterResults(self, args, items): """Filters the list results by name and URI.""" for item in items: # If no positional arguments were given, do no filtering. if not args.names: yield item # At this point, we have to do filtering because there was at # least one positional argument. elif item['selfLink'] in self.self_links or item['name'] in self.names: yield item def ComputeDynamicProperties(self, args, items): """Computes dynamic properties, which are not returned by GCE API.""" _ = args return items def Run(self, args): # This is a horrible hack to remove the resource registry from gcloud but # leave it in compute. This will go away when base_classes is removed. # Collection is checked to allow individual commands to opt-out of the magic # format setting. if not args.IsSpecified('format') and not args.uri and self.Collection(): r = resource_registry.RESOURCE_REGISTRY[self.Collection()] args.format = r.list_format return self._Run(args) def _Run(self, args): """Yields JSON-serializable dicts of resources or self links.""" # Data structures used to perform client-side filtering of # resources by their names and/or URIs. self.self_links = set() self.names = set() self.resource_refs = [] # The field selector should be constructed before any resources # are fetched, so if there are any syntactic errors with the # fields, we can fail fast. field_selector = property_selector.PropertySelector( properties=None, transformations=self.transformations) errors = [] self.PopulateResourceFilteringStructures(args) items = self.FilterResults( args, self.GetResources(args, errors)) items = lister.ProcessResults( resources=items, field_selector=field_selector) items = self.ComputeDynamicProperties(args, items) for item in items: yield item if errors: utils.RaiseToolException(errors) class GlobalLister(BaseLister): """Base class for listing global resources.""" def GetResources(self, args, errors): return lister.GetGlobalResourcesDicts( service=self.service, project=self.project, filter_expr=self.GetFilterExpr(args), http=self.http, batch_url=self.batch_url, errors=errors) def GetGlobalListerHelp(resource): """Returns the detailed help dict for a global list command.""" if resource == 'routes': detailed_help = { 'brief': 'List non-dynamic Google Compute Engine ' + resource, 'DESCRIPTION': """ *{{command}}* displays all custom static, subnet, and peering {0} in VPC networks in a project. To list custom dynamic routes learned by Cloud Routers, query the status of the Cloud Router that learned the route using `gcloud compute routers get-status`. For more details, refer to https://cloud.google.com/vpc/docs/using-routes#listingroutes. """.format(resource), 'EXAMPLES': """ To list all non-dynamic {0} in a project in table form, run: $ {{command}} To list the URIs of all non-dynamic {0} in a project, run: $ {{command}} --uri """.format(resource) } else: detailed_help = { 'brief': 'List Google Compute Engine ' + resource, 'DESCRIPTION': """ *{{command}}* displays all Google Compute Engine {0} in a project. """.format(resource), 'EXAMPLES': """ To list all {0} in a project in table form, run: $ {{command}} To list the URIs of all {0} in a project, run: $ {{command}} --uri """.format(resource) } if resource == 'images': detailed_help['EXAMPLES'] += """ To list the names of {0} older than one year from oldest to newest (`-P1Y` is an [ISO8601 duration](https://en.wikipedia.org/wiki/ISO_8601)): $ {{command}} --format="value(NAME)" --filter="creationTimestamp < -P1Y" """.format(resource) return detailed_help class RegionalLister(BaseLister): """Base class for listing regional resources.""" @staticmethod def Args(parser): BaseLister.Args(parser) parser.add_argument( '--regions', metavar='REGION', help='If provided, only resources from the given regions are queried.', type=arg_parsers.ArgList(min_length=1), default=[]) def GetResources(self, args, errors): region_names = [ self.CreateGlobalReference(region, resource_type='regions').Name() for region in args.regions] return lister.GetRegionalResourcesDicts( service=self.service, project=self.project, requested_regions=region_names, filter_expr=self.GetFilterExpr(args), http=self.http, batch_url=self.batch_url, errors=errors) def GetRegionalListerHelp(resource): """Returns the detailed help dict for a regional list command.""" return { 'brief': 'List Google Compute Engine ' + resource, 'DESCRIPTION': """ *{{command}}* displays all Google Compute Engine {0} in a project. By default, {0} from all regions are listed. The results can be narrowed down using a filter: `--filter="region:( REGION ... )"`. """.format(resource), 'EXAMPLES': """ To list all {0} in a project in table form, run: $ {{command}} To list the URIs of all {0} in a project, run: $ {{command}} --uri To list all {0} in the ``us-central1'' and ``europe-west1'' regions, run: $ {{command}} --filter="region:( us-central1 europe-west1 )" """.format(resource) } class ZonalLister(BaseLister): """Base class for listing zonal resources.""" @staticmethod def Args(parser): BaseLister.Args(parser) parser.add_argument( '--zones', metavar='ZONE', help='If provided, only resources from the given zones are queried.', type=arg_parsers.ArgList(min_length=1), completer=completers.ZonesCompleter, default=[]) def GetResources(self, args, errors): zone_names = [ self.CreateGlobalReference(zone, resource_type='zones').Name() for zone in args.zones] return lister.GetZonalResourcesDicts( service=self.service, project=self.project, requested_zones=zone_names, filter_expr=self.GetFilterExpr(args), http=self.http, batch_url=self.batch_url, errors=errors) def GetZonalListerHelp(resource): """Returns the detailed help dict for a zonal list command.""" return { 'brief': 'List Google Compute Engine ' + resource, 'DESCRIPTION': """ *{{command}}* displays all Google Compute Engine {0} in a project. By default, {0} from all zones are listed. The results can be narrowed down using a filter: `--filter="zone:( ZONE ... )"`. """.format(resource), 'EXAMPLES': """ To list all {0} in a project in table form, run: $ {{command}} To list the URIs of all {0} in a project, run: $ {{command}} --uri To list all {0} in the ``us-central1-b'' and ``europe-west1-d'' zones, run: $ {{command}} --filter="zone:( us-central1-b europe-west1-d )" """.format(resource) } class ScopeType(enum.Enum): """Scope type of compute resource.""" global_scope = 1 regional_scope = 2 zonal_scope = 3 class MultiScopeLister(BaseLister): """Base class for listing global and regional resources.""" @staticmethod def AddScopeArgs(parser, scopes): BaseLister.Args(parser) scope = parser.add_mutually_exclusive_group() if ScopeType.zonal_scope in scopes: scope.add_argument( '--zones', metavar='ZONE', help=('If provided, only zonal resources are shown. ' 'If arguments are provided, only resources from the given ' 'zones are shown.'), type=arg_parsers.ArgList()) if ScopeType.regional_scope in scopes: scope.add_argument( '--regions', metavar='REGION', help=('If provided, only regional resources are shown. ' 'If arguments are provided, only resources from the given ' 'regions are shown.'), type=arg_parsers.ArgList()) if ScopeType.global_scope in scopes: scope.add_argument( '--global', action='store_true', help='If provided, only global resources are shown.', default=False) @abc.abstractproperty def global_service(self): """The service used to list global resources.""" @abc.abstractproperty def regional_service(self): """The service used to list regional resources.""" @abc.abstractproperty def zonal_service(self): """The service used to list regional resources.""" @abc.abstractproperty def aggregation_service(self): """The service used to get aggregated list of resources.""" def GetResources(self, args, errors): """Returns zonal, regional and/or global resources. Args: args: argparse.Namespace, Parsed arguments errors: list, Errors will be returned in this list Returns: Zonal, regional and/or global resources. """ regions = getattr(args, 'regions', None) zones = getattr(args, 'zones', None) global_arg = getattr(args, 'global', None) # This is true if the user provided no flags indicating scope no_scope_flags = not regions and not zones and not global_arg requests = [] filter_expr = self.GetFilterExpr(args) if args.page_size is not None: max_results = min(args.page_size, constants.MAX_RESULTS_PER_PAGE) else: max_results = constants.MAX_RESULTS_PER_PAGE project = self.project # If --regions is present with no arguments OR no scope flags are present # then we have to do an aggregated list # pylint:disable=g-explicit-bool-comparison if no_scope_flags and self.aggregation_service: requests.append( (self.aggregation_service, 'AggregatedList', self.aggregation_service.GetRequestType('AggregatedList')( filter=filter_expr, maxResults=max_results, project=project))) # Else if some regions were provided then only list within them elif regions is not None: region_names = set( self.CreateGlobalReference(region, resource_type='regions').Name() for region in regions) for region_name in sorted(region_names): requests.append( (self.regional_service, 'List', self.regional_service.GetRequestType('List')( filter=filter_expr, maxResults=max_results, region=region_name, project=project))) # Else if some regions were provided then only list within them elif zones is not None: zone_names = set( self.CreateGlobalReference(zone, resource_type='zones').Name() for zone in zones) for zone_name in sorted(zone_names): requests.append( (self.zonal_service, 'List', self.zonal_service.GetRequestType('List')( filter=filter_expr, maxResults=max_results, zone=zone_name, project=project))) else: # Either --global was specified or we do not have aggregation service. # Note that --global, --region and --zone are mutually exclusive. requests.append( (self.global_service, 'List', self.global_service.GetRequestType('List')( filter=filter_expr, maxResults=max_results, project=project))) return request_helper.ListJson( requests=requests, http=self.http, batch_url=self.batch_url, errors=errors) def GetMultiScopeListerHelp(resource, scopes): """Returns the detailed help dict for a global and regional list command.""" zone_example_text = """ To list all {0} in zones ``us-central1-b'' and ``europe-west1-d'', given they are zonal resources, run: $ {{command}} --filter="zone:( europe-west1-d us-central1-b )" """ region_example_text = """ To list all {0} in the ``us-central1'' and ``europe-west1'' regions, given they are regional resources, run: $ {{command}} --filter="region:( europe-west1 us-central1 )" """ global_example_text = """ To list all global {0} in a project, run: $ {{command}} --global """ allowed_flags = [] default_result = [] if ScopeType.global_scope in scopes: allowed_flags.append("``--global''") default_result.append('global ' + resource) if ScopeType.regional_scope in scopes: allowed_flags.append("``--regions''") default_result.append(resource + ' from all regions') if ScopeType.zonal_scope in scopes: allowed_flags.append("``--zones''") default_result.append(resource + ' from all zones') allowed_flags_text = ( ', '.join(allowed_flags[:-1]) + ' or ' + allowed_flags[-1]) default_result_text = ( ', '.join(default_result[:-1]) + ' and ' + default_result[-1]) return { 'brief': 'List Google Compute Engine ' + resource, 'DESCRIPTION': """ *{{command}}* displays all Google Compute Engine {0} in a project. By default, {1} are listed. The results can be narrowed down by providing the {2} flag. """.format(resource, default_result_text, allowed_flags_text), 'EXAMPLES': (""" To list all {0} in a project in table form, run: $ {{command}} To list the URIs of all {0} in a project, run: $ {{command}} --uri """ + (global_example_text if ScopeType.global_scope in scopes else '') + (region_example_text if ScopeType.regional_scope in scopes else '') + (zone_example_text if ScopeType.zonal_scope in scopes else '')) .format(resource), } class GlobalRegionalLister(MultiScopeLister): """Base class for listing global and regional resources.""" SCOPES = [ScopeType.regional_scope, ScopeType.global_scope] @staticmethod def Args(parser): MultiScopeLister.AddScopeArgs(parser, GlobalRegionalLister.SCOPES) @property def aggregation_service(self): return self.regional_service @property def zonal_service(self): return None def GetGlobalRegionalListerHelp(resource): return GetMultiScopeListerHelp(resource, GlobalRegionalLister.SCOPES) class BaseDescriber(base.DescribeCommand, BaseCommand): """Base class for the describe subcommands.""" service = None @staticmethod def Args(parser, resource=None): BaseDescriber.AddArgs(parser, resource) @staticmethod def AddArgs(parser, resource=None): parser.add_argument( 'name', metavar='NAME', help='The name of the resource to fetch.') @property def method(self): return 'Get' def ScopeRequest(self, ref, request): """Adds a zone or region to the request object if necessary.""" @abc.abstractmethod def CreateReference(self, args): pass def SetNameField(self, ref, request): """Sets the field in the request that corresponds to the object name.""" name_field = self.service.GetMethodConfig(self.method).ordered_params[-1] setattr(request, name_field, ref.Name()) def ComputeDynamicProperties(self, args, items): """Computes dynamic properties, which are not returned by GCE API.""" _ = args return items def Run(self, args): """Yields JSON-serializable dicts of resources.""" ref = self.CreateReference(args) get_request_class = self.service.GetRequestType(self.method) request = get_request_class(project=getattr(ref, 'project', self.project)) self.SetNameField(ref, request) self.ScopeRequest(ref, request) get_request = (self.service, self.method, request) errors = [] objects = request_helper.MakeRequests( requests=[get_request], http=self.http, batch_url=self.batch_url, errors=errors) resource_list = lister.ProcessResults(objects, field_selector=None) resource_list = list(self.ComputeDynamicProperties(args, resource_list)) if errors: utils.RaiseToolException( errors, error_message='Could not fetch resource:') return resource_list[0] def GetMultiScopeDescriberHelp(resource, scopes): """Returns the detailed help dict for a multiscope describe command. Args: resource: resource name, singular form with no preposition scopes: global/regional/zonal or mix of them Returns: Help for multi-scope describe command. """ article = text.GetArticle(resource) zone_example_text = """\ To get details about a zonal {0} in the ``us-central1-b'' zone, run: $ {{command}} --zone=us-central1-b """ region_example_text = """\ To get details about a regional {0} in the ``us-central1'' regions, run: $ {{command}} --region=us-central1 """ global_example_text = """\ To get details about a global {0}, run: $ {{command}} --global """ return { 'brief': ('Display detailed information about {0} {1}' .format(article, resource)), 'DESCRIPTION': """ *{{command}}* displays all data associated with {0} {1} in a project. """.format(article, resource), 'EXAMPLES': ( (global_example_text if ScopeType.global_scope in scopes else '') + (region_example_text if ScopeType.regional_scope in scopes else '') + (zone_example_text if ScopeType.zonal_scope in scopes else '')) .format(resource), } HELP = textwrap.dedent("""\ You can edit the resource below. Lines beginning with "#" are ignored. If you introduce a syntactic error, you will be given the opportunity to edit the file again. You can abort by closing this file without saving it. At the bottom of this file, you will find an example resource. Only fields that can be modified are shown. The original resource with all of its fields is reproduced in the comment section at the bottom of this document. """) def SerializeDict(value, fmt): """Serializes value to either JSON or YAML.""" if fmt == 'json': return six.text_type( json.dumps( value, indent=2, sort_keys=True, separators=(str(','), str(': ')))) else: return six.text_type(yaml.dump(value)) def DeserializeValue(value, fmt): """Parses the given JSON or YAML value.""" if fmt == 'json': return json.loads(value) else: return yaml.load(value) def WriteResourceInCommentBlock(serialized_resource, title, buf): """Outputs a comment block with the given serialized resource.""" buf.write('# ') buf.write(title) buf.write('\n# ') buf.write('-' * len(title)) buf.write('\n#\n') for line in serialized_resource.splitlines(): buf.write('#') if line: buf.write(' ') buf.write(line) buf.write('\n') def IsDefaultUniverse(): """Outputs whether it is running against default universe.""" return ( properties.VALUES.core.universe_domain.Get() == properties.VALUES.core.universe_domain.default ) def SupportNetworkConnectivityTest(): """Outputs whether connectivity is supported.""" return IsDefaultUniverse() def SupportIAP(): """Outputs whether IAP is supported.""" return True