284 lines
11 KiB
Python
284 lines
11 KiB
Python
# Copyright 2019 Intel, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 http import client as http_client
|
|
import pecan
|
|
import re
|
|
import wsme
|
|
from wsme import types as wtypes
|
|
|
|
from oslo_log import log
|
|
from oslo_utils import uuidutils
|
|
|
|
from cyborg.api.controllers import base
|
|
from cyborg.api.controllers import link
|
|
from cyborg.api.controllers import types
|
|
from cyborg.api import expose
|
|
from cyborg.common import authorize_wsgi
|
|
from cyborg.common import constants
|
|
from cyborg.common import exception
|
|
from cyborg import objects
|
|
LOG = log.getLogger(__name__)
|
|
|
|
"""
|
|
The device profile object and db table has a profile_json field, which has
|
|
its own version apart from the device profile groups field. The reasoning
|
|
behind that was this structure may evolve more rapidly. Since then the
|
|
feedback has been to manage this with the API version itself, preferably
|
|
with microversions, rather than use a second version.
|
|
|
|
One problem with that is that we have to decide on a suitable db
|
|
representation for device profile groups, which form an array of
|
|
string pairs. The Cyborg community wishes to keep the number of
|
|
tables small and manageable.
|
|
|
|
As of now, the db layer for device profiles still uses the profile_json
|
|
field. But the API layer returns the device profile as it should be.
|
|
The objects layer does the conversion.
|
|
"""
|
|
|
|
|
|
class DeviceProfile(base.APIBase):
|
|
"""API representation of a device profile.
|
|
|
|
This class enforces type checking and value constraints, and converts
|
|
between the internal object model and the API representation of
|
|
a device profile. See module notes above.
|
|
"""
|
|
|
|
"""The UUID of the device profile"""
|
|
uuid = types.uuid
|
|
|
|
"""The name of the device profile"""
|
|
name = wtypes.text
|
|
|
|
"""The description of the device profile"""
|
|
description = wtypes.text
|
|
|
|
"""The groups of the device profile"""
|
|
groups = [types.jsontype]
|
|
|
|
created_at = wtypes.datetime.datetime
|
|
updated_at = wtypes.datetime.datetime
|
|
|
|
"""A list containing a self link"""
|
|
links = wsme.wsattr([link.Link], readonly=True)
|
|
|
|
def __init__(self, **kwargs):
|
|
super(DeviceProfile, self).__init__(**kwargs)
|
|
self.fields = []
|
|
for field in objects.DeviceProfile.fields:
|
|
self.fields.append(field)
|
|
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, obj_devprof):
|
|
api_devprof = cls(**obj_devprof.as_dict())
|
|
api_devprof.links = [
|
|
link.Link.make_link('self', pecan.request.public_url,
|
|
'device_profiles', api_devprof.uuid)
|
|
]
|
|
return api_devprof
|
|
|
|
|
|
class DeviceProfileCollection(base.APIBase):
|
|
"""API representation of a collection of device profiles."""
|
|
|
|
"""A list containing device profile objects"""
|
|
device_profiles = [DeviceProfile]
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, obj_devprofs):
|
|
collection = cls()
|
|
collection.device_profiles = [
|
|
DeviceProfile.convert_with_links(obj_devprof)
|
|
for obj_devprof in obj_devprofs]
|
|
return collection
|
|
|
|
|
|
class DeviceProfilesController(base.CyborgController,
|
|
DeviceProfileCollection):
|
|
"""REST controller for Device Profiles."""
|
|
|
|
@authorize_wsgi.authorize_wsgi("cyborg:device_profile", "create", False)
|
|
@expose.expose(DeviceProfile, body=types.jsontype,
|
|
status_code=http_client.CREATED)
|
|
def post(self, req_devprof_list):
|
|
"""Create one or more device_profiles.
|
|
|
|
NOTE: Only one device profile supported in Train.
|
|
|
|
:param devprof: a list of device_profiles.
|
|
[{ "name": <string>,
|
|
"groups": [ {"key1: "value1", "key2": "value2"} ]
|
|
"uuid": <uuid> # optional
|
|
}]
|
|
:returns: The list of created device profiles
|
|
"""
|
|
# TODO(Sundar) Support more than one devprof per request, if needed
|
|
|
|
LOG.info("[device_profiles] POST request = (%s)", req_devprof_list)
|
|
if len(req_devprof_list) != 1:
|
|
raise exception.InvalidParameterValue(
|
|
err="Only one device profile allowed "
|
|
"per POST request for now.")
|
|
req_devprof = req_devprof_list[0]
|
|
self._validate_post_request(req_devprof)
|
|
|
|
context = pecan.request.context
|
|
obj_devprof = objects.DeviceProfile(context, **req_devprof)
|
|
|
|
new_devprof = pecan.request.conductor_api.device_profile_create(
|
|
context, obj_devprof)
|
|
return DeviceProfile.convert_with_links(new_devprof)
|
|
|
|
def _validate_post_request(self, req_devprof):
|
|
NAME = "^[a-zA-Z0-9-_]+$"
|
|
keys = '|'.join(["resources:", "trait:", "accel:"])
|
|
GROUP_KEYS = "^(%s)" % keys
|
|
TRAIT_VALUES = ["required", "forbidden"]
|
|
|
|
name = req_devprof.get("name")
|
|
if not name:
|
|
raise exception.DeviceProfileNameNeeded()
|
|
elif not re.match(NAME, name):
|
|
raise exception.InvalidParameterValue(
|
|
err="Device profile name must be of the form %s" % NAME)
|
|
|
|
groups = req_devprof.get("groups")
|
|
if not groups:
|
|
raise exception.DeviceProfileGroupsExpected()
|
|
|
|
for group in groups:
|
|
tmp_group = copy.deepcopy(group)
|
|
for key, value in tmp_group.items():
|
|
# check resource and trait prefix format
|
|
if not re.match(GROUP_KEYS, key):
|
|
raise exception.InvalidParameterValue(
|
|
err="Device profile group keys must be of "
|
|
" the form %s" % GROUP_KEYS)
|
|
# check trait name and it's value
|
|
if key.startswith("trait:"):
|
|
inner_origin_trait = ":".join(key.split(":")[1:])
|
|
inner_trait = inner_origin_trait.strip(" ")
|
|
if not inner_trait.startswith('CUSTOM_'):
|
|
raise exception.InvalidParameterValue(
|
|
err="Unsupported trait name format %s, should "
|
|
"start with CUSTOM_" % inner_trait)
|
|
if value not in TRAIT_VALUES:
|
|
raise exception.InvalidParameterValue(
|
|
err="Unsupported trait value %s, the value must"
|
|
" be one among %s" % TRAIT_VALUES)
|
|
# strip " " and update old group key.
|
|
if inner_origin_trait != inner_trait:
|
|
del group[key]
|
|
standard_key = "trait:" + inner_trait
|
|
group[standard_key] = value
|
|
# check rc name and it's value
|
|
if key.startswith("resources:"):
|
|
inner_origin_rc = ":".join(key.split(":")[1:])
|
|
inner_rc = inner_origin_rc.strip(" ")
|
|
if inner_rc not in constants.SUPPORT_RESOURCES and \
|
|
not inner_rc.startswith('CUSTOM_'):
|
|
raise exception.InvalidParameterValue(
|
|
err="Unsupported resource class %s" % inner_rc)
|
|
try:
|
|
int(value)
|
|
except ValueError:
|
|
raise exception.InvalidParameterValue(
|
|
err="Resources nummber %s is invalid" % value)
|
|
# strip " " and update old group key.
|
|
if inner_origin_rc != inner_rc:
|
|
del group[key]
|
|
standard_key = "resources:" + inner_rc
|
|
group[standard_key] = value
|
|
|
|
def _get_device_profile_list(self, names=None, uuid=None):
|
|
"""Get a list of API objects representing device profiles."""
|
|
|
|
context = pecan.request.context
|
|
obj_devprofs = objects.DeviceProfile.list(context)
|
|
if names:
|
|
new_obj_devprofs = [devprof for devprof in obj_devprofs
|
|
if devprof['name'] in names]
|
|
obj_devprofs = new_obj_devprofs
|
|
elif uuid is not None:
|
|
new_obj_devprofs = [devprof for devprof in obj_devprofs
|
|
if devprof['uuid'] == uuid]
|
|
obj_devprofs = new_obj_devprofs
|
|
|
|
return obj_devprofs
|
|
|
|
@authorize_wsgi.authorize_wsgi("cyborg:device_profile", "get_all", False)
|
|
@expose.expose(DeviceProfileCollection, wtypes.text)
|
|
def get_all(self, name=None):
|
|
"""Retrieve a list of device profiles."""
|
|
if name is not None:
|
|
names = name.split(',')
|
|
else:
|
|
names = []
|
|
LOG.info('[device_profiles] get_all. names=%s', names)
|
|
api_obj_devprofs = self._get_device_profile_list(names)
|
|
|
|
ret = DeviceProfileCollection.convert_with_links(api_obj_devprofs)
|
|
LOG.info('[device_profiles] get_all returned: %s', ret)
|
|
return ret
|
|
|
|
@authorize_wsgi.authorize_wsgi("cyborg:device_profile", "get_one")
|
|
@expose.expose(DeviceProfile, wtypes.text)
|
|
def get_one(self, uuid):
|
|
"""Retrieve a single device profile by uuid."""
|
|
LOG.info('[device_profiles] get_one. uuid=%s', uuid)
|
|
api_obj_devprofs = self._get_device_profile_list(uuid=uuid)
|
|
if len(api_obj_devprofs) == 0:
|
|
raise exception.ResourceNotFound(
|
|
resource='Device profile',
|
|
msg='with uuid %s' % uuid)
|
|
|
|
count = len(api_obj_devprofs)
|
|
if count != 1: # Should never happen because names are unique
|
|
raise exception.ExpectedOneObject(obj='device profile',
|
|
count=count)
|
|
ret = api_obj_devprofs[0]
|
|
LOG.info('[device_profiles] get_one returned: %s', ret)
|
|
return DeviceProfile.convert_with_links(ret)
|
|
|
|
@authorize_wsgi.authorize_wsgi("cyborg:device_profile", "delete")
|
|
@expose.expose(None, wtypes.text, status_code=http_client.NO_CONTENT)
|
|
def delete(self, value):
|
|
"""Delete one or more device_profiles.
|
|
|
|
URL: /device_profiles/{uuid} OR /device_profiles?value=foo,bar
|
|
|
|
:param value: This should be one of these two:
|
|
- UUID of a device_profile.
|
|
- Comma-delimited list of device profile names.
|
|
"""
|
|
context = pecan.request.context
|
|
if uuidutils.is_uuid_like(value):
|
|
uuid = value
|
|
LOG.info('[device_profiles] delete uuid=%s', uuid)
|
|
obj_devprof = objects.DeviceProfile.get_by_uuid(context, uuid)
|
|
pecan.request.conductor_api.device_profile_delete(
|
|
context, obj_devprof)
|
|
else:
|
|
names = value.split(",")
|
|
LOG.info('[device_profiles] delete names=(%s)', names)
|
|
for name in names:
|
|
obj_devprof = objects.DeviceProfile.get_by_name(context, name)
|
|
pecan.request.conductor_api.device_profile_delete(
|
|
context, obj_devprof)
|