# -*- coding: utf-8 -*- # # Copyright 2020 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. """recommender API recommendations list command.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import itertools from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.asset import client_util from googlecloudsdk.api_lib.recommender import locations from googlecloudsdk.api_lib.recommender import recommendation from googlecloudsdk.api_lib.recommender import recommenders from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.recommender import flags from googlecloudsdk.command_lib.run import exceptions from googlecloudsdk.core import log DETAILED_HELP = { 'EXAMPLES': """ Lists recommendations for a Cloud project. $ {command} --project=project-id --location=global --recommender=google.compute.instance.MachineTypeRecommender """, } DISPLAY_FORMAT = """ table( name.basename(): label=RECOMMENDATION_ID, primaryImpact.category: label=PRIMARY_IMPACT_CATEGORY, stateInfo.state: label=RECOMMENDATION_STATE, lastRefreshTime: label=LAST_REFRESH_TIME, priority: label=PRIORITY, recommenderSubtype: label=RECOMMENDER_SUBTYPE, description: label=DESCRIPTION ) """ @base.UniverseCompatible @base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA) class List(base.ListCommand): r"""List recommendations for Google Cloud resources. This command lists all recommendations for the specified Google Cloud resource, location, and recommender. If a recommender or location is not specified, recommendations for all supported recommenders or locations, respectively, are listed. If the `--recursive` flag is set, recommendations for child resources and projects are also listed. Supported recommenders can be found here: https://cloud.google.com/recommender/docs/recommenders. """ detailed_help = DETAILED_HELP @staticmethod def Args(parser): """Args is called by calliope to gather arguments for this command. Args: parser: An argparse parser that you can use to add arguments that go on the command line after this command. """ flags.AddParentFlagsToParser(parser) parser.add_argument( '--location', metavar='LOCATION', required=False, help=( 'Location to list recommendations for. If no location is specified,' ' recommendations for all supported locations are listed.' ' Not specifying a location can add 15-20 seconds to the runtime.' ), ) parser.add_argument( '--recommender', metavar='RECOMMENDER', required=False, help=( 'Recommender to list recommendations for. If no recommender is' ' specified, recommendations for all supported recommenders are' ' listed. Supported recommenders can be found here:' ' https://cloud.google.com/recommender/docs/recommenders' ' Not specifying a recommender can add 15-20 seconds to the' ' runtime.' ), ) parser.add_argument( '--recursive', required=False, action=arg_parsers.StoreTrueFalseAction, help=(""" In addition to listing the recommendations for the specified organization or folder, recursively list all of the recommendations for the resource's child resources, including their descendants (for example, a folder's sub-folders), and for the resource's child projects. For example, when using the `--recursive` flag and specifying an organization, the response lists all of the recommendations associated with that organization, all of the recommendations associated with that organization's folders and sub-folders, and all of the recommendations associated with that organization's child projects. The maximum number of resources (organization, folders, projects, and descendant resources) that can be accessed at once with the `--recursive` flag is 100. For a larger number of nested resources, use [BigQuery export](https://cloud.google.com/recommender/docs/bq-export/export-recommendations-to-bq). Using `--recursive` can add 15-20 seconds per resource to the runtime. """), ) parser.display_info.AddFormat(DISPLAY_FORMAT) def setArgs(self, args): """Setups up args to search all resources under a project, folder, or organization. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: (argparse.Namespace) args with additional parameters setup """ args.read_mask = '*' args.asset_types = [ # gcloud-disable-gdu-domain 'cloudresourcemanager.googleapis.com/Project', # gcloud-disable-gdu-domain 'cloudresourcemanager.googleapis.com/Folder' ] args.order_by = 'createTime' args.query = '*' if args.project: args.scope = 'projects/' + args.project if args.organization: args.scope = 'organizations/' + args.organization if args.folder: args.scope = 'folders/' + args.folder return args def read(self, asset_in): if isinstance(asset_in, list): return asset_in[0] else: return asset_in def AddResource(self, resource_location) -> bool: if resource_location not in self.resource_locations: self.resource_locations.append(resource_location) return True return False def searchAllResources(self, args): """Search all nested resources under a project, folder, or organization. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: (List): a list of all Google Cloud resource,location pairs """ args = self.setArgs(args) client = client_util.AssetSearchClient(client_util.DEFAULT_API_VERSION) resources = list(client.SearchAllResources(args)) self.resource_locations = [] for r in resources: parent_resource = f'{self.read(args.scope)}/locations/{r.location}' if 'project' not in parent_resource: self.AddResource(parent_resource) # gcloud-disable-gdu-domain if r.assetType == 'cloudresourcemanager.googleapis.com/Project': self.AddResource(f'{self.read(r.project)}/locations/{r.location}') # gcloud-disable-gdu-domain if ( r.assetType == 'cloudresourcemanager.googleapis.com/Folder' and self.AddResource(f'{self.read(r.folders)}/locations/{r.location}') ): args.scope = self.read(r.folders) resources.extend(client.SearchAllResources(args)) if len(self.resource_locations) > 100: raise exceptions.UnsupportedOperationError( 'The maximum number of resources (organizations, folders, projects,' ' and descendant resources) that can be accessed to list' ' recommendations is 100. To access' ' a larger number of resources, use BigQuery Export.' ) return self.resource_locations def CollectAssets(self, args): """Iterate through search all resources response and collects unique Google Cloud resouce,location pairs. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: (List): a list of all Google Cloud resource,location pairs """ # Collect Assets and Locations log.status.Print('Collecting Resources... This may take some time...') if args.recursive: resource_locations = self.searchAllResources(args) else: if args.location is None: loc_client = locations.CreateClient(self.ReleaseTrack()) resource_locations = [ loc.name for loc in loc_client.List( args.page_size, project=args.project, organization=args.organization, folder=args.folder, billing_account=args.billing_account, ) ] else: resource_locations = [ flags.GetResourceSegment(args) + f'/locations/{args.location}' ] return resource_locations def ListRecommenders(self, args): """List all Recommenders. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: (list) all recommenders in a list of strings """ recommenders_client = recommenders.CreateClient(self.ReleaseTrack()) recommenders_response = recommenders_client.List(args.page_size) return list(recommenders_response) def GetRecommendations(self, args, asset_recommenders): """Collects all recommendations for a given Google Cloud Resource. Args: args: argparse.Namespace, The arguments that this command was invoked with. asset_recommenders: list, The list of Google Cloud resource recommender URL to collect recommendations Returns: (Recommendations) a iterator for all returned recommendations """ recommendations = [] recommendations_client = recommendation.CreateClient(self.ReleaseTrack()) resource_prev = None location_prev = None for resource, location, recommender in asset_recommenders: if resource != resource_prev or location != location_prev: log.status.Print(f'Reading Recommendations for: {resource} {location}') resource_prev = resource location_prev = location parent_name = '/'.join([resource, location, recommender]) new_recommendations = recommendations_client.List( parent_name, args.page_size ) try: # skip recommenders that the user does not have access to. peek = next(new_recommendations) # execute first element of generator except ( apitools_exceptions.HttpBadRequestError, apitools_exceptions.BadStatusCodeError, StopIteration, ): continue recommendations = itertools.chain( recommendations, (peek,), new_recommendations ) return recommendations def Run(self, args): """Run 'gcloud recommender recommendations list'. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: The list of recommendations for this project. """ # Collect Assets and Locations resource_locations = self.CollectAssets(args) # collect recommendations for all recommenders asset_recommenders = [] for asset in resource_locations: tokens = asset.split('/') resource = '/'.join(tokens[:2]) location = '/'.join(tokens[2:4]) if args.recommender is not None: asset_recommenders.append( (resource, location, f'recommenders/{args.recommender}') ) else: # loop through all recommenders asset_recommenders.extend([ (resource, location, f'recommenders/{response.name}') for response in self.ListRecommenders(args) ]) return self.GetRecommendations(args, asset_recommenders) @base.UniverseCompatible @base.ReleaseTracks(base.ReleaseTrack.GA) class ListOriginal(base.ListCommand): r"""List operations for a recommendation. This command lists all recommendations for a given Google Cloud entity ID, location, and recommender. Supported recommenders can be found here: https://cloud.google.com/recommender/docs/recommenders. The following Google Cloud entity types are supported: project, billing_account, folder and organization. """ detailed_help = DETAILED_HELP @staticmethod def Args(parser): """Args is called by calliope to gather arguments for this command. Args: parser: An argparse parser that you can use to add arguments that go on the command line after this command. """ flags.AddParentFlagsToParser(parser) parser.add_argument( '--location', metavar='LOCATION', required=True, help='Location to list recommendations for.', ) parser.add_argument( '--recommender', metavar='RECOMMENDER', required=True, help=( 'Recommender to list recommendations for. Supported recommenders' ' can be found here:' ' https://cloud.google.com/recommender/docs/recommenders.' ), ) parser.display_info.AddFormat(DISPLAY_FORMAT) def Run(self, args): """Run 'gcloud recommender recommendations list'. Args: args: argparse.Namespace, The arguments that this command was invoked with. Returns: The list of recommendations for this project. """ recommendations_client = recommendation.CreateClient(self.ReleaseTrack()) parent_name = flags.GetRecommenderName(args) return recommendations_client.List(parent_name, args.page_size)