cyborg/cyborg/api/controllers/v2/device_profiles.py

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)