processors/recommendations.py (441 lines of code) (raw):
# Copyright 2021 Google LLC
#
# 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.
from .base import Processor, NotConfiguredException
from helpers.base import Context
import json
import re
from google.cloud.recommender_v1.services.recommender import RecommenderClient
from google.cloud.recommender_v1.types.recommendation import Recommendation
from google.cloud.recommender_v1.types.insight import Insight
from google.cloud.recommender_v1.types.recommender_service import ListInsightsRequest
from googleapiclient import discovery
from google.api_core.gapic_v1 import client_info as grpc_client_info
from google.api_core.client_options import ClientOptions
import fnmatch
class UnknownRecommenderException(Exception):
pass
class RecommendationsProcessor(Processor):
multi_regions = ['global', 'us', 'europe', 'asia']
recommenders = {}
insights = {}
def __init__(self, config, jinja_environment, data, event,
context: Context):
self.recommenders = {
'google.compute.instance.MachineTypeRecommender': {
'location': [self.is_zone],
'parent': [self.is_project]
},
'google.compute.instanceGroupManager.MachineTypeRecommender': {
'location': [self.is_region, self.is_zone],
'parent': [self.is_project]
},
'google.compute.instance.IdleResourceRecommender': {
'location': [self.is_zone],
'parent': [self.is_project]
},
'google.compute.disk.IdleResourceRecommender': {
'location': [self.is_region, self.is_zone],
'parent': [self.is_project]
},
'google.compute.address.IdleResourceRecommender': {
'location': [self.is_global, self.is_region],
'parent': [self.is_project]
},
'google.compute.image.IdleResourceRecommender': {
'location': [self.is_multi_region, self.is_region],
'parent': [self.is_project]
},
'google.compute.commitment.UsageCommitmentRecommender': {
'location': [self.is_region],
'parent': [self.is_project, self.is_billing_account]
},
'google.cloudsql.instance.OutOfDiskRecommender': {
'location': [self.is_region],
'parent': [self.is_project]
},
'google.cloudsql.instance.IdleRecommender': {
'location': [self.is_region],
'parent': [self.is_project]
},
'google.cloudsql.instance.OverprovisionedRecommender': {
'location': [self.is_region],
'parent': [self.is_project]
},
'google.iam.policy.Recommender': {
'location': [self.is_global],
'parent': [
self.is_organization, self.is_folder, self.is_project
]
},
'google.resourcemanager.projectUtilization.Recommender': {
'location': [self.is_global],
'parent': [self.is_project]
},
'google.logging.productSuggestion.ContainerRecommender': {
'location': [],
'parent': [],
}, # Not supported by API currently
'google.monitoring.productSuggestion.ComputeRecommender': {
'location': [],
'parent': [],
}, # Not supported by API currently
'google.accounts.security.SecurityKeyRecommender': {
'location': [],
'parent': [],
}, # Not supported by API currently,
}
self.insights = {
'google.compute.firewall.Insight': {
'location': [self.is_global],
'parent': [self.is_project]
},
'google.iam.policy.Insight': {
'location': [self.is_global],
'parent': [
self.is_organization, self.is_folder, self.is_project
]
},
'google.iam.serviceAccount.Insight': {
'location': [self.is_global],
'parent': [self.is_project]
},
'google.resourcemanager.projectUtilization.Insight': {
'location': [self.is_global],
'parent': [self.is_project]
},
'google.cloudasset.asset.Insight': {
'location': [self.is_global],
'parent': [
self.is_organization, self.is_folder, self.is_project
]
},
'google.compute.disk.IdleResourceInsight': {
'location': [self.is_region, self.is_zone],
'parent': [self.is_project]
},
'google.compute.image.IdleResourceInsight': {
'location': [self.is_global],
'parent': [self.is_project]
},
'google.compute.address.IdleResourceInsight': {
'location': [self.is_global, self.is_region],
'parent': [self.is_project]
},
}
super().__init__(config, jinja_environment, data, event, context)
def get_regions(self, compute_service, project_id, location_filters):
"""Fetches all regions for a project"""
region_request = compute_service.regions().list(project=project_id)
regions = []
while region_request is not None:
region_response = region_request.execute()
for region in region_response['items']:
for region_filter in location_filters:
if fnmatch.fnmatch(region['name'], region_filter):
regions.append(region['name'])
break
region_request = compute_service.regions().list_next(
previous_request=region_request,
previous_response=region_response)
for multi_region in self.multi_regions:
for region_filter in location_filters:
if fnmatch.fnmatch(multi_region, region_filter):
regions.append(multi_region)
break
return regions
def is_project(self, parent):
return parent.startswith('projects/')
def is_folder(self, parent):
return parent.startswith('folders/')
def is_organization(self, parent):
return parent.startswith('organizations/')
def is_billing_account(self, parent):
return parent.startswith('billingAccounts/')
def is_global(self, region):
return True if region == 'global' else False
def is_multi_region(self, region):
return True if region in self.multi_regions else False
def is_region(self, region):
return True if re.search("\d+$", region) else False
def is_zone(self, zone):
return True if re.search("\d+-[a-z]$", zone) else False
def get_link(self, parent):
if parent[0].startswith('projects/'):
return 'project=%s' % (parent[1][1])
if parent[0].startswith('organizations/'):
return 'organization=%s' % (parent[0].split("/")[-1:])
if parent[0].startswith('folders/'):
return 'folder=%s' % (parent[0].split("/")[-1:])
return ''
def get_zones(self, compute_service, project_id, location_filters):
"""Fetches all zones for a project"""
# Fetch zones and filter
zone_request = compute_service.zones().list(project=project_id)
# Add multiregional locations
zones = []
while zone_request is not None:
zone_response = zone_request.execute()
for zone in zone_response['items']:
for zone_filter in location_filters:
if fnmatch.fnmatch(zone['name'], zone_filter):
zones.append(zone['name'])
break
zone_request = compute_service.zones().list_next(
previous_request=zone_request, previous_response=zone_response)
# Make sure we have unique values only
zones = list(set(zones))
return zones
def get_recommendations(self, client, recommender_types, parents,
all_locations, filter):
"""Fetches recommendations with specified recommender types from applicable locations"""
recommendations = {}
for parent in parents:
recommendations[parent[0]] = []
for location in all_locations:
for recommender_type in recommender_types:
ok_parent = False
for test in self.recommenders[recommender_type]['parent']:
if test(parent[0]):
ok_parent = True
break
if not ok_parent:
continue
ok_location = False
for test in self.recommenders[recommender_type]['location']:
if test(location):
ok_location = True
break
if not ok_location:
continue
full_parent = '%s/locations/%s/recommenders/%s' % (
parent[0], location, recommender_type)
self.logger.debug('Fetching recommendations...',
extra={
'type': parent[0].split('/')[0],
'parent': full_parent,
'location': location,
'recommender': recommender_type
})
rec_response = client.list_recommendations(
parent=full_parent, filter=filter)
for recommendation in rec_response:
_rec = {
'type':
parent[0].split('/')[0],
'parent':
parent[1],
'link':
self.get_link(parent),
'location':
location,
'recommender_type':
recommender_type,
'recommendation':
Recommendation.to_dict(recommendation),
}
recommendations[parent[0]].append(_rec)
return recommendations
def get_insights(self, client, insight_types, parents, all_locations,
filter):
"""Fetches insights with specified insight types from applicable locations"""
insights = {}
for parent in parents:
insights[parent[0]] = []
for location in all_locations:
for insight_type in insight_types:
ok_parent = False
for test in self.insights[insight_type]['parent']:
if test(parent[0]):
ok_parent = True
break
if not ok_parent:
continue
ok_location = False
for test in self.insights[insight_type]['location']:
if test(location):
ok_location = True
break
if not ok_location:
continue
full_parent = '%s/locations/%s/insightTypes/%s' % (
parent[0], location, insight_type)
self.logger.debug('Fetching insights...',
extra={
'type': parent[0].split('/')[0],
'parent': full_parent,
'location': location,
'insight_type': insight_type
})
insights_request = ListInsightsRequest(parent=full_parent,
filter=filter)
insights_response = client.list_insights(
request=insights_request)
for insight in insights_response:
_insight = {
'type': parent[0].split('/')[0],
'parent': parent[1],
'link': self.get_link(parent),
'location': location,
'insight_type': insight_type,
'insight': Insight.to_dict(insight),
}
insights[parent[0]].append(_insight)
return insights
def rollup_recommendations(self, recommendations):
recommendations_rollup = {}
for parent, recs in recommendations.items():
if parent not in recommendations_rollup:
recommendations_rollup[parent] = {}
for _rec in recs:
rec = _rec['recommendation']
if 'primary_impact' in rec and 'recommender_subtype' in rec:
sub_type = rec['recommender_subtype']
if 'cost_projection' in rec['primary_impact']:
cost_projection = rec['primary_impact'][
'cost_projection']['cost']
if sub_type not in recommendations_rollup[parent]:
recommendations_rollup[parent][sub_type] = {
'link': _rec['link'],
'parent': _rec['parent'],
'type': _rec['type'],
'count': 0,
'cost': {
'currency_code': '',
'nanos': 0,
'units': 0
}
}
recommendations_rollup[parent][sub_type]['count'] += 1
recommendations_rollup[parent][sub_type]['cost'][
'currency_code'] = cost_projection['currency_code']
recommendations_rollup[parent][sub_type]['cost'][
'nanos'] += int(cost_projection['nanos'])
recommendations_rollup[parent][sub_type]['cost'][
'units'] += int(cost_projection['units'])
return recommendations_rollup
def rollup_insights(self, insights):
insights_rollup = {}
for parent, _insights in insights.items():
if parent not in insights_rollup:
insights_rollup[parent] = {}
for _insight in _insights:
insight = _insight['insight']
if 'insight_subtype' in insight:
sub_type = insight['insight_subtype']
if sub_type not in insights_rollup[parent]:
insights_rollup[parent][sub_type] = {
'link': _insight['link'],
'parent': _insight['parent'],
'type': _insight['type'],
'count': 0,
}
insights_rollup[parent][sub_type]['count'] += 1
return insights_rollup
def get_default_config_key():
return 'recommendations'
def process(
self,
output_var={
'recommendations': 'recommendations',
'recommendations_rollup': 'recommendations_rollup',
'insights': 'insights',
'insights_rollup': 'insights_rollup'
}):
recommender_config = self.config
for recommender in recommender_config['recommender_types']:
if recommender not in self.recommenders:
raise UnknownRecommenderException(
'Unknown recommender %s specified in config!' %
(recommender))
data = json.loads(self.data)
self.jinja_environment.globals = {
**self.jinja_environment.globals,
**data
}
projects = []
if 'projects' in recommender_config:
projects = self._jinja_var_to_list(recommender_config['projects'],
'projects')
folders = []
if 'folders' in recommender_config:
folders = self._jinja_var_to_list(recommender_config['folders'],
'folders')
organizations = []
if 'organizations' in recommender_config:
organizations = self._jinja_var_to_list(
recommender_config['organizations'], 'organizations')
billing_accounts = []
if 'billingAccounts' in recommender_config:
billing_accounts = self._jinja_var_to_list(
recommender_config['billingAccounts'], 'billing_accounts')
if len(projects) == 0 and len(folders) == 0 and len(
organizations) == 0 and len(billing_accounts) == 0:
raise NotConfiguredException(
'No projects, organizations, folders or billing accounts specified in config!'
)
location_filters = self._jinja_var_to_list(
recommender_config['locations'], 'locations')
if len(location_filters) == 0:
raise NotConfiguredException(
'No location filters specified in config!')
quota_project_id = recommender_config[
'quota_project_id'] if 'quota_project_id' in recommender_config else None
client_info = grpc_client_info.ClientInfo(
user_agent=self._get_user_agent())
client_options = ClientOptions(quota_project_id=quota_project_id)
client = RecommenderClient(client_info=client_info,
client_options=client_options)
compute_service = discovery.build('compute',
'v1',
http=self._get_branded_http(),
client_options=client_options)
if len(projects) == 0 and not quota_project_id:
raise NotConfiguredException(
'Please specify at least one project (or quota project ID) to fetch regions and zones.'
)
all_zones = self.get_zones(
compute_service,
quota_project_id if quota_project_id else projects[0],
location_filters)
all_regions = self.get_regions(
compute_service,
quota_project_id if quota_project_id else projects[0],
location_filters)
all_locations = all_zones + all_regions
self.logger.debug('Fetched all available locations.',
extra={'locations': all_locations})
parents = []
for project in self.expand_projects(projects):
parents.append(('projects/%s' % project[1], project))
for organization in organizations:
parents.append(('organizations/%s' % organization, [organization]))
for folder in folders:
parents.append(('folder/%s' % folder, [folder]))
for billing_account in billing_accounts:
parents.append(
('billingAccounts/%s' % billing_account, [billing_account]))
self.logger.debug('Determined all parents.', extra={'parents': parents})
recommendations = {}
recommendations_rollup = {}
if 'fetch_recommendations' in recommender_config:
fetch_recommendations = self._jinja_expand_bool(
recommender_config['fetch_recommendations'])
if fetch_recommendations:
recommendations = self.get_recommendations(
client, recommender_config['recommender_types'], parents,
all_locations, recommender_config['recommendation_filter']
if 'recommendation_filter' in recommender_config else None)
recommendations_rollup = self.rollup_recommendations(
recommendations)
insights = {}
insights_rollup = {}
if 'fetch_insights' in recommender_config:
fetch_insights = self._jinja_expand_bool(
recommender_config['fetch_insights'])
if fetch_insights:
insights = self.get_insights(
client, recommender_config['insight_types'], parents,
all_locations, recommender_config['insight_filter']
if 'insight_filter' in recommender_config else None)
insights_rollup = self.rollup_insights(insights)
self.logger.debug('Fetching recommendations and/or insights finished.')
_ret = {
output_var['recommendations']: recommendations,
output_var['recommendations_rollup']: recommendations_rollup,
output_var['insights']: insights,
output_var['insights_rollup']: insights_rollup,
}
if 'vars' in recommender_config:
return {**recommender_config['vars'], **_ret}
return _ret