Add Flavor API
Change-Id: I26a0671d3a7431c52de898e3ea91c890dc3eb287
This commit is contained in:
parent
26e2299908
commit
06e72f347b
@ -22,6 +22,7 @@ import pecan
|
|||||||
from gyan.api.controllers import base as controllers_base
|
from gyan.api.controllers import base as controllers_base
|
||||||
from gyan.api.controllers import link
|
from gyan.api.controllers import link
|
||||||
from gyan.api.controllers.v1 import hosts as host_controller
|
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.v1 import ml_models as ml_model_controller
|
||||||
from gyan.api.controllers import versions as ver
|
from gyan.api.controllers import versions as ver
|
||||||
from gyan.api import http_error
|
from gyan.api import http_error
|
||||||
@ -59,7 +60,8 @@ class V1(controllers_base.APIBase):
|
|||||||
'media_types',
|
'media_types',
|
||||||
'links',
|
'links',
|
||||||
'hosts',
|
'hosts',
|
||||||
'ml_models'
|
'ml_models',
|
||||||
|
'flavors'
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -81,6 +83,12 @@ class V1(controllers_base.APIBase):
|
|||||||
pecan.request.host_url,
|
pecan.request.host_url,
|
||||||
'hosts', '',
|
'hosts', '',
|
||||||
bookmark=True)]
|
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,
|
v1.ml_models = [link.make_link('self', pecan.request.host_url,
|
||||||
'ml-models', ''),
|
'ml-models', ''),
|
||||||
link.make_link('bookmark',
|
link.make_link('bookmark',
|
||||||
@ -95,6 +103,7 @@ class Controller(controllers_base.Controller):
|
|||||||
|
|
||||||
hosts = host_controller.HostController()
|
hosts = host_controller.HostController()
|
||||||
ml_models = ml_model_controller.MLModelController()
|
ml_models = ml_model_controller.MLModelController()
|
||||||
|
flavors = flavor_controller.FlavorController()
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get(self):
|
def get(self):
|
||||||
@ -148,7 +157,7 @@ class Controller(controllers_base.Controller):
|
|||||||
'method': pecan.request.method,
|
'method': pecan.request.method,
|
||||||
'body': pecan.request.body})
|
'body': pecan.request.body})
|
||||||
# LOG.debug(msg)
|
# LOG.debug(msg)
|
||||||
LOG.debug(args)
|
# LOG.debug(args)
|
||||||
return super(Controller, self)._route(args)
|
return super(Controller, self)._route(args)
|
||||||
|
|
||||||
|
|
||||||
|
210
gyan/api/controllers/v1/flavors.py
Normal file
210
gyan/api/controllers/v1/flavors.py
Normal file
@ -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
|
58
gyan/api/controllers/v1/schemas/flavors.py
Normal file
58
gyan/api/controllers/v1/schemas/flavors.py
Normal file
@ -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
|
||||||
|
}
|
@ -47,6 +47,50 @@ ml_model_name = {
|
|||||||
'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
|
'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 = {
|
hex_uuid = {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'maxLength': 32,
|
'maxLength': 32,
|
||||||
|
46
gyan/api/controllers/v1/views/flavors_view.py
Normal file
46
gyan/api/controllers/v1/views/flavors_view.py
Normal file
@ -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()))
|
@ -14,11 +14,13 @@ import itertools
|
|||||||
|
|
||||||
from gyan.common.policies import host
|
from gyan.common.policies import host
|
||||||
from gyan.common.policies import base
|
from gyan.common.policies import base
|
||||||
|
from gyan.common.policies import flavor
|
||||||
from gyan.common.policies import ml_model
|
from gyan.common.policies import ml_model
|
||||||
|
|
||||||
def list_rules():
|
def list_rules():
|
||||||
return itertools.chain(
|
return itertools.chain(
|
||||||
base.list_rules(),
|
base.list_rules(),
|
||||||
host.list_rules(),
|
host.list_rules(),
|
||||||
ml_model.list_rules()
|
ml_model.list_rules(),
|
||||||
|
flavor.list_rules()
|
||||||
)
|
)
|
||||||
|
112
gyan/common/policies/flavor.py
Normal file
112
gyan/common/policies/flavor.py
Normal file
@ -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
|
@ -160,6 +160,14 @@ def get_ml_model(ml_model_ident):
|
|||||||
|
|
||||||
return ml_model
|
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):
|
def validate_ml_model_state(ml_model, action):
|
||||||
if ml_model.status not in VALID_STATES[action]:
|
if ml_model.status not in VALID_STATES[action]:
|
||||||
raise exception.InvalidStateException(
|
raise exception.InvalidStateException(
|
||||||
|
@ -115,6 +115,88 @@ def update_ml_model(context, ml_model_id, values):
|
|||||||
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")
|
@profiler.trace("db")
|
||||||
def list_compute_hosts(context, filters=None, limit=None, marker=None,
|
def list_compute_hosts(context, filters=None, limit=None, marker=None,
|
||||||
sort_key=None, sort_dir=None):
|
sort_key=None, sort_dir=None):
|
||||||
|
@ -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 ###
|
@ -305,3 +305,80 @@ class Connection(object):
|
|||||||
filter_names = ['uuid', 'project_id', 'user_id']
|
filter_names = ['uuid', 'project_id', 'user_id']
|
||||||
return self._add_filters(query, models.ML_Model, filters=filters,
|
return self._add_filters(query, models.ML_Model, filters=filters,
|
||||||
filter_names=filter_names)
|
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)
|
@ -141,3 +141,20 @@ class ComputeHost(Base):
|
|||||||
hostname = Column(String(255), nullable=False)
|
hostname = Column(String(255), nullable=False)
|
||||||
status = Column(String(255), nullable=False)
|
status = Column(String(255), nullable=False)
|
||||||
type = 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)
|
||||||
|
@ -11,13 +11,16 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from gyan.objects import compute_host
|
from gyan.objects import compute_host
|
||||||
|
from gyan.objects import flavor
|
||||||
from gyan.objects import ml_model
|
from gyan.objects import ml_model
|
||||||
|
|
||||||
|
|
||||||
ComputeHost = compute_host.ComputeHost
|
ComputeHost = compute_host.ComputeHost
|
||||||
|
Flavor = flavor.Flavor
|
||||||
ML_Model = ml_model.ML_Model
|
ML_Model = ml_model.ML_Model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ComputeHost',
|
'ComputeHost',
|
||||||
'ML_Model'
|
'ML_Model',
|
||||||
|
'Flavor'
|
||||||
)
|
)
|
||||||
|
161
gyan/objects/flavor.py
Normal file
161
gyan/objects/flavor.py
Normal file
@ -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])
|
Loading…
Reference in New Issue
Block a user