diff --git a/gyan/api/controllers/v1/__init__.py b/gyan/api/controllers/v1/__init__.py index c4d908e..a5e0787 100644 --- a/gyan/api/controllers/v1/__init__.py +++ b/gyan/api/controllers/v1/__init__.py @@ -22,6 +22,7 @@ import pecan from gyan.api.controllers import base as controllers_base from gyan.api.controllers import link from gyan.api.controllers.v1 import hosts as host_controller +from gyan.api.controllers.v1 import flavors as flavor_controller from gyan.api.controllers.v1 import ml_models as ml_model_controller from gyan.api.controllers import versions as ver from gyan.api import http_error @@ -59,7 +60,8 @@ class V1(controllers_base.APIBase): 'media_types', 'links', 'hosts', - 'ml_models' + 'ml_models', + 'flavors' ) @staticmethod @@ -81,6 +83,12 @@ class V1(controllers_base.APIBase): pecan.request.host_url, 'hosts', '', bookmark=True)] + v1.flavors = [link.make_link('self', pecan.request.host_url, + 'flavors', ''), + link.make_link('bookmark', + pecan.request.host_url, + 'flavors', '', + bookmark=True)] v1.ml_models = [link.make_link('self', pecan.request.host_url, 'ml-models', ''), link.make_link('bookmark', @@ -95,6 +103,7 @@ class Controller(controllers_base.Controller): hosts = host_controller.HostController() ml_models = ml_model_controller.MLModelController() + flavors = flavor_controller.FlavorController() @pecan.expose('json') def get(self): @@ -148,7 +157,7 @@ class Controller(controllers_base.Controller): 'method': pecan.request.method, 'body': pecan.request.body}) # LOG.debug(msg) - LOG.debug(args) + # LOG.debug(args) return super(Controller, self)._route(args) diff --git a/gyan/api/controllers/v1/flavors.py b/gyan/api/controllers/v1/flavors.py new file mode 100644 index 0000000..433b24d --- /dev/null +++ b/gyan/api/controllers/v1/flavors.py @@ -0,0 +1,210 @@ +# 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. + +import base64 +import shlex +import json + +from oslo_log import log as logging +from oslo_utils import strutils +from oslo_utils import uuidutils +import pecan +import six + +from gyan.api.controllers import base +from gyan.api.controllers import link +from gyan.api.controllers.v1 import collection +from gyan.api.controllers.v1.schemas import flavors as schema +from gyan.api.controllers.v1.views import flavors_view as view +from gyan.api import utils as api_utils +from gyan.api import validation +from gyan.common import consts +from gyan.common import context as gyan_context +from gyan.common import exception +from gyan.common.i18n import _ +from gyan.common.policies import flavor as policies +from gyan.common import policy +from gyan.common import utils +import gyan.conf +from gyan import objects + +CONF = gyan.conf.CONF +LOG = logging.getLogger(__name__) + + +def check_policy_on_flavor(flavor, action): + context = pecan.request.context + policy.enforce(context, action, flavor, action=action) + + +class FlavorCollection(collection.Collection): + """API representation of a collection of flavors.""" + + fields = { + 'flavors', + 'next' + } + + """A list containing flavor objects""" + + def __init__(self, **kwargs): + super(FlavorCollection, self).__init__(**kwargs) + self._type = 'flavors' + + @staticmethod + def convert_with_links(rpc_flavors, limit, url=None, + expand=False, **kwargs): + context = pecan.request.context + collection = FlavorCollection() + collection.flavors = \ + [view.format_flavor(url, p) + for p in rpc_flavors] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class FlavorController(base.Controller): + """Controller for Flavors.""" + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def get_all(self, **kwargs): + """Retrieve a list of flavors. + + """ + context = pecan.request.context + policy.enforce(context, "flavor:get_all", + action="flavor:get_all") + return self._get_flavors_collection(**kwargs) + + def _get_flavors_collection(self, **kwargs): + context = pecan.request.context + if utils.is_all_projects(kwargs): + policy.enforce(context, "flavor:get_all_all_projects", + action="flavor:get_all_all_projects") + context.all_projects = True + kwargs.pop('all_projects', None) + limit = api_utils.validate_limit(kwargs.pop('limit', None)) + sort_dir = api_utils.validate_sort_dir(kwargs.pop('sort_dir', 'asc')) + sort_key = kwargs.pop('sort_key', 'id') + resource_url = kwargs.pop('resource_url', None) + expand = kwargs.pop('expand', None) + + flavor_allowed_filters = ['name', 'cpu', 'python_version', 'driver', + 'memory', 'disk', 'additional_details'] + filters = {} + for filter_key in flavor_allowed_filters: + if filter_key in kwargs: + policy_action = policies.FLAVOR % ('get_one:' + filter_key) + context.can(policy_action, might_not_exist=True) + filter_value = kwargs.pop(filter_key) + filters[filter_key] = filter_value + marker_obj = None + marker = kwargs.pop('marker', None) + if marker: + marker_obj = objects.Flavor.get_by_uuid(context, + marker) + if kwargs: + unknown_params = [str(k) for k in kwargs] + msg = _("Unknown parameters: %s") % ", ".join(unknown_params) + raise exception.InvalidValue(msg) + + flavors = objects.Flavor.list(context, + limit, + marker_obj, + sort_key, + sort_dir, + filters=filters) + return FlavorCollection.convert_with_links(flavors, limit, + url=resource_url, + expand=expand, + sort_key=sort_key, + sort_dir=sort_dir) + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def get_one(self, flavor_ident, **kwargs): + """Retrieve information about the given flavor. + + :param flavor_ident: UUID or name of a flavor. + """ + context = pecan.request.context + if utils.is_all_projects(kwargs): + policy.enforce(context, "flavor:get_one_all_projects", + action="flavor:get_one_all_projects") + context.all_projects = True + flavor = utils.get_flavor(flavor_ident) + check_policy_on_flavor(flavor.as_dict(), "flavor:get_one") + return view.format_flavor(pecan.request.host_url, + flavor) + + @base.Controller.api_version("1.0") + @pecan.expose('json') + @api_utils.enforce_content_types(['application/json']) + @exception.wrap_pecan_controller_exception + @validation.validated(schema.flavor_create) + def post(self, **flavor_dict): + return self._do_post(**flavor_dict) + + def _do_post(self, **flavor_dict): + """Create or run a new flavor. + + :param flavor_dict: a flavor within the request body. + """ + context = pecan.request.context + policy.enforce(context, "flavor:create", + action="flavor:create") + + LOG.debug("bhaaaaaaaaaaaaaaaaaaaaaaaaaaa") + LOG.debug(flavor_dict) + flavor_dict["additional_details"] = json.dumps(flavor_dict["additional_details"]) + LOG.debug(flavor_dict) + # flavor_dict["model_data"] = open("/home/bharath/model.zip", "rb").read() + new_flavor = objects.Flavor(context, **flavor_dict) + flavor = new_flavor.create(context) + LOG.debug(new_flavor) + # compute_api.flavor_create(context, new_flavor) + # Set the HTTP Location Header + pecan.response.location = link.build_url('flavors', + flavor.id) + pecan.response.status = 201 + return view.format_flavor(pecan.request.host_url, + flavor) + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def patch(self, flavor_ident, **patch): + """Update an existing flavor. + + :param flavor_ident: UUID or name of a flavor. + :param patch: a json PATCH document to apply to this flavor. + """ + context = pecan.request.context + flavor = utils.get_flavor(flavor_ident) + check_policy_on_flavor(flavor.as_dict(), "flavor:update") + return view.format_flavor(context, pecan.request.host_url, + flavor.as_dict()) + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + @validation.validate_query_param(pecan.request, schema.query_param_delete) + def delete(self, flavor_ident, **kwargs): + """Delete a flavor. + + :param flavor_ident: UUID or Name of a Flavor. + :param force: If True, allow to force delete the Flavor. + """ + context = pecan.request.context + flavor = utils.get_flavor(flavor_ident) + check_policy_on_flavor(flavor.as_dict(), "flavor:delete") + flavor.destroy(context) + pecan.response.status = 204 \ No newline at end of file diff --git a/gyan/api/controllers/v1/schemas/flavors.py b/gyan/api/controllers/v1/schemas/flavors.py new file mode 100644 index 0000000..b21b82f --- /dev/null +++ b/gyan/api/controllers/v1/schemas/flavors.py @@ -0,0 +1,58 @@ +# 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. + +import copy + +from gyan.api.controllers.v1.schemas import parameter_types + +_flavor_properties = {} + +flavor_create = { + 'type': 'object', + 'properties': { + "name": parameter_types.flavor_name, + "driver": parameter_types.flavor_driver, + "cpu": parameter_types.flavor_cpu, + "disk": parameter_types.flavor_disk, + 'memory': parameter_types.flavor_memory, + 'python_version': parameter_types.flavor_python_version, + 'additional_details': parameter_types.flavor_additional_details + + }, + 'required': ['name', 'cpu', 'memory', 'python_version', 'disk', 'driver', 'additional_details'], + 'additionalProperties': False +} + + +query_param_create = { + 'type': 'object', + 'properties': { + 'run': parameter_types.boolean_extended + }, + 'additionalProperties': False +} + +ml_model_update = { + 'type': 'object', + 'properties': {}, + 'additionalProperties': False +} + +query_param_delete = { + 'type': 'object', + 'properties': { + 'force': parameter_types.boolean_extended, + 'all_projects': parameter_types.boolean_extended, + 'stop': parameter_types.boolean_extended + }, + 'additionalProperties': False +} diff --git a/gyan/api/controllers/v1/schemas/parameter_types.py b/gyan/api/controllers/v1/schemas/parameter_types.py index fa1f64c..601cccb 100644 --- a/gyan/api/controllers/v1/schemas/parameter_types.py +++ b/gyan/api/controllers/v1/schemas/parameter_types.py @@ -47,6 +47,50 @@ ml_model_name = { 'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' } +flavor_name = { + 'type': ['string', 'null'], + 'minLength': 2, + 'maxLength': 255, + 'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' +} + +flavor_cpu = { + 'type': ['number', 'integer', 'null'], + 'minLength': 2, + 'maxLength': 255 +} + +flavor_driver = { + 'type': ['string', 'null'], + 'minLength': 2, + 'maxLength': 255, + 'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' +} + +flavor_disk = { + 'type': ['string', 'null'], + 'minLength': 2, + 'maxLength': 255, +} + +flavor_memory = { + 'type': ['string', 'null'], + 'minLength': 2, + 'maxLength': 255 +} + +flavor_additional_details = { + 'type': ['object', 'null'], + 'minLength': 2, + 'maxLength': 255 +} + +flavor_python_version = { + 'type': ['string', 'null', 'number', 'integer'], + 'minLength': 2, + 'maxLength': 255 +} + hex_uuid = { 'type': 'string', 'maxLength': 32, diff --git a/gyan/api/controllers/v1/views/flavors_view.py b/gyan/api/controllers/v1/views/flavors_view.py new file mode 100644 index 0000000..49545c6 --- /dev/null +++ b/gyan/api/controllers/v1/views/flavors_view.py @@ -0,0 +1,46 @@ +# +# 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. + +import itertools + +from gyan.api.controllers import link + + +_basic_keys = ( + 'id', + 'name', + 'cpu', + 'memory', + 'disk', + 'driver', + 'additional_details' +) + + +def format_flavor(url, flavor): + def transform(key, value): + if key not in _basic_keys: + return + if key == 'id': + yield ('id', value) + yield ('links', [link.make_link( + 'self', url, 'flavors', value), + link.make_link( + 'bookmark', url, + 'flavors', value, + bookmark=True)]) + else: + yield (key, value) + + return dict(itertools.chain.from_iterable( + transform(k, v) for k, v in flavor.as_dict().items())) diff --git a/gyan/common/policies/__init__.py b/gyan/common/policies/__init__.py index fd2d970..0bc014b 100644 --- a/gyan/common/policies/__init__.py +++ b/gyan/common/policies/__init__.py @@ -14,11 +14,13 @@ import itertools from gyan.common.policies import host from gyan.common.policies import base +from gyan.common.policies import flavor from gyan.common.policies import ml_model def list_rules(): return itertools.chain( base.list_rules(), host.list_rules(), - ml_model.list_rules() + ml_model.list_rules(), + flavor.list_rules() ) diff --git a/gyan/common/policies/flavor.py b/gyan/common/policies/flavor.py new file mode 100644 index 0000000..8c60ec6 --- /dev/null +++ b/gyan/common/policies/flavor.py @@ -0,0 +1,112 @@ +# 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 oslo_policy import policy + +from gyan.common.policies import base + +FLAVOR = 'flavor:%s' + +rules = [ + policy.DocumentedRuleDefault( + name=FLAVOR % 'create', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Create a new Flavor.', + operations=[ + { + 'path': '/v1/flavors', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'delete', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Delete a Flavor.', + operations=[ + { + 'path': '/v1/flavors/{flavor_ident}', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'delete_all_projects', + check_str=base.RULE_ADMIN_API, + description='Delete a flavors from all projects.', + operations=[ + { + 'path': '/v1/flavors/{flavor_ident}', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'delete_force', + check_str=base.RULE_ADMIN_API, + description='Forcibly delete a Flavor.', + operations=[ + { + 'path': '/v1/flavors/{flavor_ident}', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'get_one', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Retrieve the details of a specific ml model.', + operations=[ + { + 'path': '/v1/flavors/{flavor_ident}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'get_all', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Retrieve the details of all ml models.', + operations=[ + { + 'path': '/v1/flavors', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'get_all_all_projects', + check_str=base.RULE_ADMIN_API, + description='Retrieve the details of all ml models across projects.', + operations=[ + { + 'path': '/v1/flavors', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=FLAVOR % 'update', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Update a ML Model.', + operations=[ + { + 'path': '/v1/flavors/{flavor_ident}', + 'method': 'PATCH' + } + ] + ), +] + + +def list_rules(): + return rules \ No newline at end of file diff --git a/gyan/common/utils.py b/gyan/common/utils.py index 0dd9ba9..289cbbf 100644 --- a/gyan/common/utils.py +++ b/gyan/common/utils.py @@ -160,6 +160,14 @@ def get_ml_model(ml_model_ident): return ml_model +def get_flavor(flavor_ident): + flavor = api_utils.get_resource('Flavor', flavor_ident) + if not flavor: + pecan.abort(404, ('Not found; the ml model you requested ' + 'does not exist.')) + + return flavor + def validate_ml_model_state(ml_model, action): if ml_model.status not in VALID_STATES[action]: raise exception.InvalidStateException( diff --git a/gyan/db/api.py b/gyan/db/api.py index 63f4535..3025d23 100644 --- a/gyan/db/api.py +++ b/gyan/db/api.py @@ -115,6 +115,88 @@ def update_ml_model(context, ml_model_id, values): context, ml_model_id, values) +@profiler.trace("db") +def list_flavors(context, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + """List matching Flavors. + + Return a list of the specified columns for all flavors that match + the specified filters. + + :param context: The security context + :param filters: Filters to apply. Defaults to None. + :param limit: Maximum number of flavors to return. + :param marker: the last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted. + :param sort_dir: Direction in which results should be sorted. + (asc, desc) + :returns: A list of tuples of the specified columns. + """ + return _get_dbdriver_instance().list_flavors( + context, filters, limit, marker, sort_key, sort_dir) + + +@profiler.trace("db") +def create_flavor(context, values): + """Create a new Flavor. + + :param context: The security context + :param values: A dict containing several items used to identify + and track the ML Model + :returns: A ML Model. + """ + return _get_dbdriver_instance().create_flavor(context, values) + + +@profiler.trace("db") +def get_flavor_by_uuid(context, flavor_uuid): + """Return a Flavor. + + :param context: The security context + :param flavor_uuid: The uuid of a flavor. + :returns: A Flavor. + """ + return _get_dbdriver_instance().get_flavor_by_uuid( + context, flavor_uuid) + + +@profiler.trace("db") +def get_flavor_by_name(context, flavor_name): + """Return a Flavor. + + :param context: The security context + :param flavor_name: The name of a Flavor. + :returns: A Flavor. + """ + return _get_dbdriver_instance().get_flavor_by_name( + context, flavor_name) + + +@profiler.trace("db") +def destroy_flavor(context, flavor_id): + """Destroy a flavor and all associated interfaces. + + :param context: Request context + :param flavor_id: The id or uuid of a flavor. + """ + return _get_dbdriver_instance().destroy_flavor(context, flavor_id) + + +@profiler.trace("db") +def update_flavor(context, flavor_id, values): + """Update properties of a flavor. + + :param context: Request context + :param flavor_id: The id or uuid of a flavor. + :param values: The properties to be updated + :returns: A Flavor. + :raises: FlavorNotFound + """ + return _get_dbdriver_instance().update_flavor( + context, flavor_id, values) + + @profiler.trace("db") def list_compute_hosts(context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): diff --git a/gyan/db/sqlalchemy/alembic/versions/395aff469925_add_flavor_table.py b/gyan/db/sqlalchemy/alembic/versions/395aff469925_add_flavor_table.py new file mode 100644 index 0000000..67f0495 --- /dev/null +++ b/gyan/db/sqlalchemy/alembic/versions/395aff469925_add_flavor_table.py @@ -0,0 +1,34 @@ +"""Add flavor table + +Revision ID: 395aff469925 +Revises: f3bf9414f399 +Create Date: 2018-10-22 07:53:38.240884 + +""" + +# revision identifiers, used by Alembic. +revision = '395aff469925' +down_revision = 'f3bf9414f399' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('flavor', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('python_version', sa.String(length=255), nullable=False), + sa.Column('cpu', sa.String(length=255), nullable=False), + sa.Column('driver', sa.String(length=255), nullable=False), + sa.Column('memory', sa.String(length=255), nullable=False), + sa.Column('disk', sa.String(length=255), nullable=False), + sa.Column('additional_details', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### diff --git a/gyan/db/sqlalchemy/api.py b/gyan/db/sqlalchemy/api.py index 9f1cb38..a40444a 100644 --- a/gyan/db/sqlalchemy/api.py +++ b/gyan/db/sqlalchemy/api.py @@ -305,3 +305,80 @@ class Connection(object): filter_names = ['uuid', 'project_id', 'user_id'] return self._add_filters(query, models.ML_Model, filters=filters, filter_names=filter_names) + + def list_flavors(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + query = model_query(models.Flavor) + query = self._add_flavors_filters(query, filters) + LOG.debug(filters) + return _paginate_query(models.Flavor, limit, marker, + sort_key, sort_dir, query) + + def create_flavor(self, context, values): + # ensure defaults are present for new flavors + if not values.get('id'): + values['id'] = uuidutils.generate_uuid() + flavor = models.Flavor() + flavor.update(values) + try: + flavor.save() + except db_exc.DBDuplicateEntry: + raise exception.FlavorAlreadyExists(field='UUID', + value=values['uuid']) + return flavor + + def get_flavor_by_uuid(self, context, flavor_uuid): + query = model_query(models.Flavor) + query = self._add_project_filters(context, query) + query = query.filter_by(id=flavor_uuid) + try: + return query.one() + except NoResultFound: + raise exception.FlavorNotFound(flavor=flavor_uuid) + + def get_flavor_by_name(self, context, flavor_name): + query = model_query(models.Flavor) + query = self._add_project_filters(context, query) + query = query.filter_by(name=flavor_name) + try: + return query.one() + except NoResultFound: + raise exception.FlavorNotFound(flavor=flavor_name) + except MultipleResultsFound: + raise exception.Conflict('Multiple flavors exist with same ' + 'name. Please use the flavor uuid ' + 'instead.') + + def destroy_flavor(self, context, flavor_id): + session = get_session() + with session.begin(): + query = model_query(models.Flavor, session=session) + query = add_identity_filter(query, flavor_id) + count = query.delete() + if count != 1: + raise exception.FlavorNotFound(flavor_id) + + def update_flavor(self, context, flavor_id, values): + if 'id' in values: + msg = _("Cannot overwrite UUID for an existing ML Model.") + raise exception.InvalidParameterValue(err=msg) + + return self._do_update_flavor_id(flavor_id, values) + + def _do_update_flavor_id(self, flavor_id, values): + session = get_session() + with session.begin(): + query = model_query(models.Flavor, session=session) + query = add_identity_filter(query, flavor_id) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.FlavorNotFound(flavor=flavor_id) + + ref.update(values) + return ref + + def _add_flavors_filters(self, query, filters): + filter_names = ['id'] + return self._add_filters(query, models.Flavor, filters=filters, + filter_names=filter_names) \ No newline at end of file diff --git a/gyan/db/sqlalchemy/models.py b/gyan/db/sqlalchemy/models.py index 31c1362..7b0765d 100644 --- a/gyan/db/sqlalchemy/models.py +++ b/gyan/db/sqlalchemy/models.py @@ -141,3 +141,20 @@ class ComputeHost(Base): hostname = Column(String(255), nullable=False) status = Column(String(255), nullable=False) type = Column(String(255), nullable=False) + + +class Flavor(Base): + """Represents a Flavor. """ + + __tablename__ = 'flavor' + __table_args__ = ( + table_args() + ) + id = Column(String(36), primary_key=True, nullable=False) + name = Column(String(255), nullable=False) + python_version = Column(String(255), nullable=False) + cpu = Column(String(255), nullable=False) + driver = Column(String(255), nullable=False) + memory = Column(String(255), nullable=False) + disk = Column(String(255), nullable=False) + additional_details = Column(Text, nullable=False) diff --git a/gyan/objects/__init__.py b/gyan/objects/__init__.py index 9586a6a..d63a711 100644 --- a/gyan/objects/__init__.py +++ b/gyan/objects/__init__.py @@ -11,13 +11,16 @@ # under the License. from gyan.objects import compute_host +from gyan.objects import flavor from gyan.objects import ml_model ComputeHost = compute_host.ComputeHost +Flavor = flavor.Flavor ML_Model = ml_model.ML_Model __all__ = ( 'ComputeHost', - 'ML_Model' + 'ML_Model', + 'Flavor' ) diff --git a/gyan/objects/flavor.py b/gyan/objects/flavor.py new file mode 100644 index 0000000..a1be25a --- /dev/null +++ b/gyan/objects/flavor.py @@ -0,0 +1,161 @@ +# 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 oslo_log import log as logging +from oslo_versionedobjects import fields + +from gyan.common import exception +from gyan.common.i18n import _ +from gyan.db import api as dbapi +from gyan.objects import base +from gyan.objects import fields as z_fields + + +LOG = logging.getLogger(__name__) + + +@base.GyanObjectRegistry.register +class Flavor(base.GyanPersistentObject, base.GyanObject): + VERSION = '1' + + fields = { + 'id': fields.UUIDField(nullable=True), + 'name': fields.StringField(nullable=True), + 'cpu': fields.StringField(nullable=True), + 'memory': fields.StringField(nullable=True), + 'python_version': fields.StringField(nullable=True), + 'disk': fields.BooleanField(nullable=True), + 'additional_details': fields.StringField(nullable=True), + 'created_at': fields.DateTimeField(tzinfo_aware=False, nullable=True), + 'updated_at': fields.DateTimeField(tzinfo_aware=False, nullable=True), + 'driver': z_fields.ModelField(nullable=True) + } + + @staticmethod + def _from_db_object(flavor, db_flavor): + """Converts a database entity to a formal object.""" + for field in flavor.fields: + setattr(flavor, field, db_flavor[field]) + + flavor.obj_reset_changes() + return flavor + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [Flavor._from_db_object(cls(context), obj) + for obj in db_objects] + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find a ml model based on uuid and return a :class:`ML_Model` object. + + :param uuid: the uuid of a ml model. + :param context: Security context + :returns: a :class:`ML_Model` object. + """ + db_flavor = dbapi.get_flavor_by_uuid(context, uuid) + flavor = Flavor._from_db_object(cls(context), db_flavor) + return flavor + + @base.remotable_classmethod + def get_by_name(cls, context, name): + """Find a flavor based on name and return a Flavor object. + + :param name: the logical name of a ml model. + :param context: Security context + :returns: a :class:`ML_Model` object. + """ + db_flavor = dbapi.get_flavor_by_name(context, name) + flavor = Flavor._from_db_object(cls(context), db_flavor) + return flavor + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, + sort_key=None, sort_dir=None, filters=None): + """Return a list of Flavor objects. + + :param context: Security context. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :param filters: filters when list ml models, the filter name could be + 'name', 'project_id', 'user_id'. + :returns: a list of :class:`ML_Model` object. + + """ + db_flavors = dbapi.list_flavors( + context, limit=limit, marker=marker, sort_key=sort_key, + sort_dir=sort_dir, filters=filters) + return Flavor._from_db_object_list(db_flavors, cls, context) + + def create(self, context): + """Create a Flavor record in the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: ML_Model(context) + + """ + values = self.obj_get_changes() + db_flavor = dbapi.create_flavor(context, values) + return self._from_db_object(self, db_flavor) + + @base.remotable + def destroy(self, context=None): + """Delete the Flavor from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: ML Model(context) + """ + dbapi.destroy_flavor(context, self.id) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this Flavor. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: ML Model(context) + """ + updates = self.obj_get_changes() + dbapi.update_ml_model(context, self.id, updates) + + self.obj_reset_changes() + + def obj_load_attr(self, attrname): + if not self._context: + raise exception.OrphanedObjectError(method='obj_load_attr', + objtype=self.obj_name()) + + LOG.debug("Lazy-loading '%(attr)s' on %(name)s uuid %(uuid)s", + {'attr': attrname, + 'name': self.obj_name(), + 'uuid': self.uuid, + }) + + self.obj_reset_changes([attrname])