Skip to content

Commit

Permalink
Fixes #RHIROS-1231, #RHIROS-1172, #RHIROS-1175, #RHIROS-1177 (#339)
Browse files Browse the repository at this point in the history
- Store suggested instance type and price in separate columns
- New suggested_instance_types api endpoint with sorting and filtering capabilities.
  • Loading branch information
upadhyeammit authored Oct 9, 2023
1 parent b6c7c74 commit 0d6bbc0
Show file tree
Hide file tree
Showing 13 changed files with 799 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Update top_candidate and top_candidate_price for existing records of PerformanceProfile
Revision ID: 93262eab7d77
Revises: c6f2cd708cea
Create Date: 2023-07-31 19:41:07.252297
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '93262eab7d77'
down_revision = 'c6f2cd708cea'
branch_labels = None
depends_on = None


def upgrade():
try:
print('Updating top_candidate and top_candidate_price for existing PerformanceProfile records!')
op.execute(
"update performance_profile set top_candidate = subquery.top_candidate, top_candidate_price = cast("
"subquery.top_candidate_price as double precision) from (select rule_hit_details #> '{0,details,"
"candidates,0,0}' as top_candidate, rule_hit_details #> '{0,details,candidates,0,"
"1}' as top_candidate_price, system_id from performance_profile) as subquery where "
"performance_profile.system_id = subquery.system_id")
except sa.exc.SQLAlchemyError as err:
print(f"Failed to update table with error {err}!")
else:
print('Successfully updated top_candidate and top_candidate_price for existing PerformanceProfile records!')
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add top_candidate, top_candidate_price on PerformanceProfile
Revision ID: c6f2cd708cea
Revises: 1051e0d7e62e
Create Date: 2023-07-31 19:39:02.205466
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'c6f2cd708cea'
down_revision = '1051e0d7e62e'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('performance_profile', schema=None) as batch_op:
batch_op.add_column(sa.Column('top_candidate', sa.String(length=25), nullable=True))
batch_op.add_column(sa.Column('top_candidate_price', sa.Float(), nullable=True))
batch_op.create_index('top_candidate_idx', ['top_candidate'], unique=False)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('performance_profile', schema=None) as batch_op:
batch_op.drop_index('top_candidate_idx')
batch_op.drop_column('top_candidate_price')
batch_op.drop_column('top_candidate')

# ### end Alembic commands ###
13 changes: 13 additions & 0 deletions ros/api/common/instance_types_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from ros.lib.aws_instance_types import INSTANCE_TYPES
from ros.extensions import cache


@cache.cached(timeout=0)
def instance_types_desc_dict():
instance_and_descriptions = {}
for instance, info in INSTANCE_TYPES.items():
processor = info['extra']['physicalProcessor']
v_cpu = info['extra']['vcpu']
memory = info['extra']['memory']
instance_and_descriptions[instance] = f"{processor} instance with {v_cpu} vCPUs and {memory} RAM"
return instance_and_descriptions
16 changes: 16 additions & 0 deletions ros/api/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ros.extensions import db
from sqlalchemy import exc
from ros.api.common.add_group_filter import group_filtered_query
from sqlalchemy import asc, desc

LOG = logging.getLogger(__name__)
prefix = "VALIDATE REQUEST"
Expand Down Expand Up @@ -90,3 +91,18 @@ def validate_request(*args, **kwargs):
return func(*args, **new_kwargs)

return validate_request


def sorting_order(order_how):
"""Sorting order method."""
method_name = None
if order_how == 'asc':
method_name = asc
elif order_how == 'desc':
method_name = desc
else:
abort(
403,
message="Incorrect sorting order. Possible values - ASC/DESC"
)
return method_name
2 changes: 2 additions & 0 deletions ros/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .v1.openapi_spec import OpenAPISpec
from .v1.status import Status
from .v1.call_to_action import CallToActionApi
from .v1.suggested_instance_types import SuggestedInstanceTypes
from .v1.hosts import (
HostsApi,
HostDetailsApi,
Expand All @@ -24,3 +25,4 @@ def initialize_routes(api):
api.add_resource(OpenAPISpec, '/api/ros/v1/openapi.json')
api.add_resource(CallToActionApi, '/api/ros/v1/call_to_action')
api.add_resource(ExecutiveReportAPI, '/api/ros/v1/executive_report')
api.add_resource(SuggestedInstanceTypes, '/api/ros/v1/suggested_instance_types')
20 changes: 3 additions & 17 deletions ros/api/v1/hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from ros.lib.constants import SubStates

from datetime import datetime, timedelta, timezone
from sqlalchemy import asc, desc, nullslast, nullsfirst
from sqlalchemy import asc, nullslast, nullsfirst
from flask_restful import Resource, abort, fields, marshal_with
from ros.api.common.add_group_filter import group_filtered_query
from ros.api.common.utils import sorting_order

from ros.lib.models import (
db,
Expand Down Expand Up @@ -208,24 +209,9 @@ def build_system_filters():
filters.append(System.groups[0]['name'].astext.in_(group_names))
return filters

@staticmethod
def sorting_order(order_how):
"""Sorting order method."""
method_name = None
if order_how == 'asc':
method_name = asc
elif order_how == 'desc':
method_name = desc
else:
abort(
403,
message="Incorrect sorting order. Possible values - ASC/DESC"
)
return method_name

def build_sort_expression(self, order_how, order_method):
"""Build sort expression."""
sort_order = self.sorting_order(order_how)
sort_order = sorting_order(order_how)

if order_method == 'display_name':
return (sort_order(System.display_name),
Expand Down
98 changes: 98 additions & 0 deletions ros/api/v1/suggested_instance_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from flask import request
from sqlalchemy import func
from flask_restful import Resource, fields, marshal_with, abort
from ros.api.common.add_group_filter import group_filtered_query
from ros.lib.utils import (
system_ids_by_org_id,
org_id_from_identity_header,
)

from ros.lib.models import (
db,
PerformanceProfile,
)

from ros.api.common.pagination import (
limit_value,
offset_value,
build_paginated_system_list_response
)
from ros.api.common.instance_types_helper import instance_types_desc_dict
from ros.api.common.utils import sorting_order


def non_null_suggested_instance_types():
org_id = org_id_from_identity_header(request)
systems_query = group_filtered_query(system_ids_by_org_id(org_id))
return db.session.query(PerformanceProfile.top_candidate,
func.count(PerformanceProfile.system_id).label('system_count')).filter(
PerformanceProfile.top_candidate.is_not(None)).filter(
PerformanceProfile.system_id.in_(systems_query)).group_by(PerformanceProfile.top_candidate)


class SuggestedInstanceTypes(Resource):
data = {
'instance_type': fields.String,
'cloud_provider': fields.String,
'system_count': fields.Integer,
'description': fields.String,
}
meta_fields = {
'count': fields.Integer,
'limit': fields.Integer,
'offset': fields.Integer
}
links_fields = {
'first': fields.String,
'last': fields.String,
'next': fields.String,
'previous': fields.String
}
output = {
'meta': fields.Nested(meta_fields),
'links': fields.Nested(links_fields),
'data': fields.List(fields.Nested(data))
}

@marshal_with(output)
def get(self):
limit = limit_value()
offset = offset_value()
order_by = (
request.args.get('order_by') or 'system_count'
).strip().lower()
order_how = (request.args.get('order_how') or 'desc').strip().lower()
sort_expression = self.build_sort_expression(order_how, order_by)
query = non_null_suggested_instance_types().filter(*self.build_instance_filters()).order_by(*sort_expression)
count = query.count()
query = query.limit(limit).offset(offset)
query_result = query.all()
suggested_instance_types = []
for row in query_result:
# FIXME: As of now we only support AWS cloud, so statically adding it to the dict. Fix this code block
# upon supporting multiple clouds.
record = {'instance_type': row.top_candidate, 'cloud_provider': 'AWS', 'system_count': row.system_count,
'description': instance_types_desc_dict()[row.top_candidate]}
suggested_instance_types.append(record)
return build_paginated_system_list_response(limit, offset, suggested_instance_types, count)

@staticmethod
def build_instance_filters():
filters = []
if filter_instance_type := request.args.get('instance_type'):
filters.append(PerformanceProfile.top_candidate.ilike(f'%{filter_instance_type}'))
# TODO: once ROS has multi cloud support, add cloud provider filter
return filters

def build_sort_expression(self, order_how, order_method):
"""Build sort expression."""
sort_order = sorting_order(order_how)

if order_method == 'instance_type':
return (sort_order(PerformanceProfile.top_candidate),)

if order_method == 'system_count':
return (sort_order(func.count(PerformanceProfile.system_id)),)

abort(403, message="Unexpected sort method {}".format(order_method))
return None
3 changes: 3 additions & 0 deletions ros/lib/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class PerformanceProfile(db.Model):
number_of_recommendations = db.Column(db.Integer)
system_id = db.Column(db.Integer)
psi_enabled = db.Column(db.Boolean)
top_candidate = db.Column(db.String(25), nullable=True)
top_candidate_price = db.Column(db.Float)
__table_args__ = (
db.PrimaryKeyConstraint('system_id', name='performance_profile_pkey'),
db.ForeignKeyConstraint(
Expand All @@ -26,6 +28,7 @@ class PerformanceProfile(db.Model):
ondelete='CASCADE'),
db.Index('non_optimized_system_profiles', number_of_recommendations, unique=False,
postgresql_where=(number_of_recommendations > 0)),
db.Index('top_candidate_idx', top_candidate)
)

@property
Expand Down
30 changes: 26 additions & 4 deletions ros/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,37 @@ def insert_performance_profiles(session, system_id, fields):
performance_profile_history table.
"""
fields = {} if fields is None else fields
session.execute(db.delete(PerformanceProfile).filter(PerformanceProfile.system_id == system_id))
session.commit()
fields_for_perf_profile_history = redact_instance_type_and_price(fields)

for model_class in [PerformanceProfile, PerformanceProfileHistory]:
new_entry = model_class(**fields)
old_profile_record = session.query(PerformanceProfile).filter_by(
system_id=system_id).first()
if old_profile_record:
session.delete(old_profile_record)
session.commit()

model_fields_map = {PerformanceProfile: fields,
PerformanceProfileHistory: fields_for_perf_profile_history}

for model_class, model_fields in model_fields_map.items():
new_entry = model_class(**model_fields)
session.add(new_entry)
session.flush()


def redact_instance_type_and_price(fields):
"""Remove unwanted attributes for performance profile history.
Such as - top_candidate & top_candidate_price.
"""
fields_for_perf_profile_history = {}
try:
fields_for_perf_profile_history = fields.copy()
del fields_for_perf_profile_history['top_candidate']
del fields_for_perf_profile_history['top_candidate_price']
except KeyError as err:
LOG.debug(f'Key(s) not found in fields: {err}')
return fields_for_perf_profile_history


def count_per_state(queryset, custom_filters: dict):
return queryset.filter_by(**custom_filters).count() if queryset else 0

Expand Down
Loading

0 comments on commit 0d6bbc0

Please sign in to comment.