docker/services/saving/saving_service.py (168 lines of code) (raw):
import copy
from datetime import datetime
from typing import List
from commons.constants import ACTION_SHUTDOWN
from commons.log_helper import get_logger
from services.saving.abstract_saving import AbstractSaving
from services.saving.saving import Saving
from services.saving.saving_result import SavingResult
from services.saving.split_saving import SplitSaving, SplitInstance
from services.shape_price_service import ShapePriceService
HOURS_IN_MONTH = 730.5
SECONDS_IN_DAY = 86400
_LOG = get_logger('r8s-saving_service')
class SavingService:
def __init__(self, shape_price_service: ShapePriceService):
self.shape_price_service = shape_price_service
self.action_calculator_mapping = {
'SCHEDULE': self._calculate_schedule_saving,
'SCALE_DOWN': self._calculate_resize_saving,
'SCALE_UP': self._calculate_resize_saving,
'CHANGE_SHAPE': self._calculate_resize_saving,
'SHUTDOWN': self._calculate_shutdown_saving,
'SPLIT': self._calculate_split_saving
}
def calculate_savings(self, general_actions, current_shape: str,
recommended_shapes, schedule,
customer, region, os, price_type='on_demand'):
saving_options: List[AbstractSaving] = []
general_actions = copy.copy(general_actions)
# as an alternative for shutdown
if 'SHUTDOWN' in general_actions:
general_actions.append('SCALE_DOWN')
current_monthly_price = self._get_monthly_price(
instance_type=current_shape, customer=customer,
region=region, price_type=price_type, os=os)
if not current_monthly_price:
_LOG.warning(f'No shape price found for current shape '
f'\'{current_shape}\'')
return {}
for action in general_actions:
calculator_func = self.action_calculator_mapping.get(action)
if not calculator_func:
continue
kwargs = {
'action': action,
'current_instance_type': current_shape,
'recommended_shapes': recommended_shapes,
'schedule': schedule,
'current_monthly_price': current_monthly_price,
'customer': customer,
'region': region,
'price_type': price_type,
'os': os
}
try:
result = calculator_func(**kwargs)
if not result:
continue
except:
_LOG.error(f'Exception occurred while calculating saving '
f'for action \'{action}\'')
continue
if isinstance(result, list):
saving_options.extend(result)
elif isinstance(result, AbstractSaving):
saving_options.append(result)
saving_options.sort(key=lambda k: k.saving_month_usd, reverse=True)
saving_result = SavingResult(
current_instance_type=current_shape,
current_monthly_price_usd=current_monthly_price,
saving_options=saving_options
)
return saving_result.as_dict()
def _calculate_schedule_saving(self, action, schedule,
current_monthly_price, **kwargs):
total_coverage_percent = 0.0
for schedule_item in schedule:
coverage_percent = self._get_schedule_coverage_percent(
schedule=schedule_item)
# schedules do not intercept
total_coverage_percent += coverage_percent
new_monthly_price = round(current_monthly_price *
total_coverage_percent, 2)
saving = Saving(
action=action,
old_month_price=current_monthly_price,
new_month_price=new_monthly_price
)
return saving
def _calculate_resize_saving(self, action, recommended_shapes,
current_monthly_price, customer, region,
os, price_type, **kwargs):
savings = []
for shape_data in recommended_shapes:
instance_type = shape_data.get('name')
monthly_price = self._get_monthly_price(
instance_type=instance_type,
customer=customer,
region=region,
price_type=price_type,
os=os)
if monthly_price:
saving = Saving(
action=action,
old_month_price=current_monthly_price,
new_month_price=monthly_price,
target_instance_type=instance_type
)
savings.append(saving)
return savings
@staticmethod
def _calculate_shutdown_saving(current_monthly_price, **kwargs):
return Saving(
action=ACTION_SHUTDOWN,
old_month_price=current_monthly_price,
new_month_price=0
)
def _calculate_split_saving(self, current_instance_type,
recommended_shapes, current_monthly_price,
customer, region, os, price_type, **kwargs):
split_instances = []
total_monthly_price = 0.0
for shape_data in recommended_shapes:
probability = shape_data.get('probability')
if not probability:
continue
instance_type = shape_data.get('name')
monthly_price = self._get_monthly_price(
instance_type=instance_type,
customer=customer, region=region,
price_type=price_type, os=os)
monthly_price = round(monthly_price * probability, 2)
total_monthly_price += monthly_price
split_instance = SplitInstance(
instance_type=instance_type,
monthly_price_usd=monthly_price,
probability=probability
)
split_instances.append(split_instance)
return SplitSaving(
old_month_price=current_monthly_price,
new_month_price=round(total_monthly_price, 2),
split_instances=split_instances
)
def _get_monthly_price(self, instance_type,
customer, region, os, price_type):
shape_price = self.shape_price_service.get(
customer=customer,
name=instance_type,
region=region,
os=os
)
if not shape_price:
return
hour_price = getattr(shape_price, price_type, None)
if not hour_price:
return
return round(hour_price * HOURS_IN_MONTH, 2)
@staticmethod
def _get_schedule_coverage_percent(schedule: dict):
weekdays = schedule.get('weekdays')
weekdays = list(set(weekdays))
weekdays_coverage_percent = len(weekdays) / 7
start_str = schedule.get('start')
stop_str = schedule.get('stop')
start_dt = datetime.strptime(start_str, '%H:%M')
stop_dt = datetime.strptime(stop_str, '%H:%M')
schedule_duration_seconds = int((stop_dt - start_dt).total_seconds())
day_coverage_percent = schedule_duration_seconds / SECONDS_IN_DAY
return round(weekdays_coverage_percent * day_coverage_percent, 2)