349 lines
14 KiB
Python
349 lines
14 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 pecan
|
|
from six.moves import http_client
|
|
import wsme
|
|
from wsme import types as wtypes
|
|
|
|
from oslo_log import log
|
|
|
|
from cyborg.api.controllers import base
|
|
from cyborg.api.controllers import link
|
|
from cyborg.api.controllers import types
|
|
from cyborg.api.controllers.v2 import utils
|
|
from cyborg.api.controllers.v2 import versions
|
|
from cyborg.api import expose
|
|
from cyborg.common import constants
|
|
from cyborg.common import exception
|
|
from cyborg.common.i18n import _
|
|
from cyborg.common import policy
|
|
from cyborg import objects
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class ARQ(base.APIBase):
|
|
"""API representation of an ARQ.
|
|
|
|
This class enforces type checking and value constraints, and converts
|
|
between the internal object model and the API representation.
|
|
"""
|
|
uuid = types.uuid
|
|
"""The UUID of the device profile"""
|
|
|
|
state = wtypes.text # obvious meanings
|
|
device_profile_name = wtypes.text
|
|
device_profile_group_id = wtypes.IntegerType()
|
|
|
|
hostname = wtypes.text
|
|
"""The host name to which the ARQ is bound, if any"""
|
|
|
|
device_rp_uuid = wtypes.text
|
|
"""The UUID of the bound device RP, if any"""
|
|
|
|
instance_uuid = wtypes.text
|
|
"""The UUID of the instance associated with this ARQ, if any"""
|
|
project_id = wtypes.text
|
|
"""The UUID of the instance project_id associated with this ARQ, if any"""
|
|
|
|
attach_handle_type = wtypes.text
|
|
attach_handle_info = {wtypes.text: wtypes.text}
|
|
|
|
links = wsme.wsattr([link.Link], readonly=True)
|
|
"""A list containing a self link"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super(ARQ, self).__init__(**kwargs)
|
|
self.fields = []
|
|
for field in objects.ARQ.fields:
|
|
self.fields.append(field)
|
|
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, obj_arq):
|
|
api_arq = cls(**obj_arq.as_dict())
|
|
api_arq.links = [
|
|
link.Link.make_link('self', pecan.request.public_url,
|
|
'accelerator_requests', api_arq.uuid)
|
|
]
|
|
return api_arq
|
|
|
|
|
|
class ARQCollection(base.APIBase):
|
|
"""API representation of a collection of arqs."""
|
|
|
|
arqs = [ARQ]
|
|
"""A list containing arq objects"""
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, obj_arqs):
|
|
collection = cls()
|
|
collection.arqs = [ARQ.convert_with_links(obj_arq)
|
|
for obj_arq in obj_arqs]
|
|
return collection
|
|
|
|
|
|
class ARQsController(base.CyborgController):
|
|
"""REST controller for ARQs.
|
|
|
|
For the relationship betweens ARQs and device profiles, see
|
|
nova/nova/accelerator/cyborg.py.
|
|
"""
|
|
|
|
def _get_devprof(self, context, devprof_name):
|
|
"""Get the contents of a device profile.
|
|
Since this is just a read, it is ok for the API layer
|
|
to do this, instead of the conductor.
|
|
"""
|
|
try:
|
|
obj_devprof = objects.DeviceProfile.get_by_name(context,
|
|
devprof_name)
|
|
return obj_devprof
|
|
except Exception:
|
|
return None
|
|
|
|
@policy.authorize_wsgi("cyborg:arq", "create", False)
|
|
@expose.expose(ARQCollection, body=types.jsontype,
|
|
status_code=http_client.CREATED)
|
|
def post(self, req):
|
|
"""Create one or more ARQs for a single device profile.
|
|
Request body:
|
|
{ 'device_profile_name': <string> }
|
|
Future:
|
|
{ 'device_profile_name': <string> # required
|
|
'device_profile_group_id': <integer>, # opt, default=0
|
|
'image_uuid': <glance-image-UUID>, #optional, for future
|
|
}
|
|
:param req: request body.
|
|
"""
|
|
LOG.info("[arq] post req = (%s)", req)
|
|
context = pecan.request.context
|
|
devprof = None
|
|
dp_name = req.get('device_profile_name')
|
|
if dp_name is not None:
|
|
devprof = self._get_devprof(context, dp_name)
|
|
if devprof is None:
|
|
raise exception.ResourceNotFound(
|
|
resource='Device Profile',
|
|
msg='with name=%s' % dp_name)
|
|
else:
|
|
raise exception.DeviceProfileNameNeeded()
|
|
LOG.info('[arqs] post. device profile name=%s', dp_name)
|
|
|
|
extarq_list = []
|
|
for group_id, group in enumerate(devprof.groups):
|
|
accel_resources = [
|
|
int(val) for key, val in group.items()
|
|
if key.startswith('resources')]
|
|
# If/when we introduce non-accelerator resources, like
|
|
# device-local memory, the key search above needs to be
|
|
# made specific to accelerator resources only.
|
|
num_accels = sum(accel_resources)
|
|
arq_fields = {
|
|
'device_profile_name': devprof.name,
|
|
'device_profile_group_id': group_id,
|
|
}
|
|
for i in range(num_accels):
|
|
obj_arq = objects.ARQ(context, **arq_fields)
|
|
extarq_fields = {'arq': obj_arq}
|
|
obj_extarq = objects.ExtARQ(context, **extarq_fields)
|
|
# TODO(Sundar) The conductor must do all db writes
|
|
new_extarq = obj_extarq.create(context, devprof.id)
|
|
extarq_list.append(new_extarq)
|
|
|
|
ret = ARQCollection.convert_with_links(
|
|
[extarq.arq for extarq in extarq_list])
|
|
LOG.info('[arqs] post returned: %s', ret)
|
|
return ret
|
|
|
|
@policy.authorize_wsgi("cyborg:arq", "get_one")
|
|
@expose.expose(ARQ, wtypes.text)
|
|
def get_one(self, uuid):
|
|
"""Get a single ARQ by UUID."""
|
|
context = pecan.request.context
|
|
extarq = objects.ExtARQ.get(context, uuid)
|
|
return ARQ.convert_with_links(extarq.arq)
|
|
|
|
@policy.authorize_wsgi("cyborg:arq", "get_all", False)
|
|
@expose.expose(ARQCollection, wtypes.text, types.uuid)
|
|
def get_all(self, bind_state=None, instance=None):
|
|
"""Retrieve a list of arqs."""
|
|
# TODO(Sundar) Need to implement 'arq=uuid1,...' query parameter
|
|
LOG.info('[arqs] get_all. bind_state:(%s), instance:(%s)',
|
|
bind_state or '', instance or '')
|
|
context = pecan.request.context
|
|
extarqs = objects.ExtARQ.list(context)
|
|
state_map = constants.ARQ_BIND_STATES_STATUS_MAP
|
|
valid_bind_states = list(state_map.keys())
|
|
arqs = [extarq.arq for extarq in extarqs]
|
|
# TODO(Sundar): Optimize by doing the filtering in the db layer
|
|
# Apply instance filter before state filter.
|
|
if bind_state:
|
|
if bind_state != 'resolved':
|
|
raise exception.ARQInvalidState(state=bind_state)
|
|
if instance:
|
|
new_arqs = [arq for arq in arqs
|
|
if arq['instance_uuid'] == instance]
|
|
arqs = new_arqs
|
|
if bind_state:
|
|
for arq in new_arqs:
|
|
if arq['state'] not in valid_bind_states:
|
|
# NOTE(Sundar) This should return HTTP code 423
|
|
# if any ARQ for this instance is not resolved.
|
|
LOG.warning('Some of ARQs for instance %s is not '
|
|
'resolved', instance)
|
|
return wsme.api.Response(
|
|
None,
|
|
status_code=http_client.LOCKED)
|
|
elif bind_state:
|
|
arqs = [arq for arq in arqs
|
|
if arq['state'] in valid_bind_states]
|
|
|
|
ret = ARQCollection.convert_with_links(arqs)
|
|
LOG.info('[arqs:get_all] Returned: %s', ret)
|
|
return ret
|
|
|
|
@policy.authorize_wsgi("cyborg:arq", "delete", False)
|
|
@expose.expose(None, wtypes.text, wtypes.text,
|
|
status_code=http_client.NO_CONTENT)
|
|
def delete(self, arqs=None, instance=None):
|
|
"""Delete one or more ARQS.
|
|
|
|
The request can be either one of these two forms:
|
|
DELETE /v2/accelerator_requests?arqs=uuid1,uuid2,...
|
|
DELETE /v2/accelerator_requests?instance=uuid
|
|
|
|
The second form is idempotent, i.e., it would have the same effect
|
|
if called repeatedly with the same instance UUID. In other words,
|
|
it would not raise an error on the second and later attempts even if
|
|
the first one has deleted the ARQs. Whereas the first form is not
|
|
idempotent: if one or more of the ARQs do not exist, it would raise
|
|
an error. Nova uses the second form: so repeated calls do not cause
|
|
issues.
|
|
|
|
:param arq: List of ARQ UUIDs
|
|
:param instance: UUID of instance whose ARQs need to be deleted
|
|
"""
|
|
context = pecan.request.context
|
|
if (arqs and instance) or ((not arqs) and (not instance)):
|
|
raise exception.ObjectActionError(
|
|
action='delete',
|
|
reason='Provide either an ARQ uuid list or an instance UUID')
|
|
elif arqs:
|
|
LOG.info("[arqs] delete. arqs=(%s)", arqs)
|
|
arqlist = arqs.split(',')
|
|
objects.ExtARQ.delete_by_uuid(context, arqlist)
|
|
else: # instance is not None
|
|
LOG.info("[arqs] delete. instance=(%s)", instance)
|
|
objects.ExtARQ.delete_by_instance(context, instance)
|
|
|
|
def _validate_arq_patch(self, patch):
|
|
"""Validate a single patch for an ARQ.
|
|
|
|
:param patch: a JSON PATCH document.
|
|
The patch must be of the form [{..}], as specified in the
|
|
value field of arq_uuid in patch() method below.
|
|
:returns: dict of valid fields
|
|
"""
|
|
valid_fields = {'hostname': None,
|
|
'device_rp_uuid': None,
|
|
'instance_uuid': None}
|
|
if utils.allow_project_id():
|
|
valid_fields['project_id'] = None
|
|
if ((not all(p['op'] == 'add' for p in patch)) and
|
|
(not all(p['op'] == 'remove' for p in patch))):
|
|
raise exception.PatchError(
|
|
reason='Every op must be add or remove')
|
|
|
|
for p in patch:
|
|
path = p['path'].lstrip('/')
|
|
if path == 'project_id' and not utils.allow_project_id():
|
|
raise exception.NotAcceptable(_(
|
|
"Request not acceptable. The minimal required API "
|
|
"version should be %(base)s.%(opr)s") %
|
|
{'base': versions.BASE_VERSION,
|
|
'opr': versions.MINOR_1_PROJECT_ID})
|
|
if path not in valid_fields.keys():
|
|
reason = 'Invalid path in patch {}'.format(p['path'])
|
|
raise exception.PatchError(reason=reason)
|
|
if p['op'] == 'add':
|
|
valid_fields[path] = p['value']
|
|
not_found = [field for field, value in valid_fields.items()
|
|
if value is None]
|
|
if patch[0]['op'] == 'add' and len(not_found) > 0:
|
|
msg = ','.join(not_found)
|
|
reason = _('Fields absent in patch {}').format(msg)
|
|
raise exception.PatchError(reason=reason)
|
|
|
|
return valid_fields
|
|
|
|
@staticmethod
|
|
def _check_if_already_bound(context, valid_fields):
|
|
patch_fields = list(valid_fields.values())[0]
|
|
instance_uuid = patch_fields['instance_uuid']
|
|
extarqs = objects.ExtARQ.list(context)
|
|
extarqs_for_instance = [
|
|
extarq for extarq in extarqs
|
|
if extarq.arq['instance_uuid'] == instance_uuid]
|
|
if extarqs_for_instance: # duplicate binding request
|
|
msg = _('Instance {} already has accelerator requests. '
|
|
'Cannot bind additional ARQs.')
|
|
reason = msg.format(instance_uuid)
|
|
raise exception.PatchError(reason=reason)
|
|
|
|
@policy.authorize_wsgi("cyborg:arq", "update", False)
|
|
@expose.expose(None, body=types.jsontype,
|
|
status_code=http_client.ACCEPTED)
|
|
def patch(self, patch_list):
|
|
"""Bind/Unbind one or more ARQs.
|
|
|
|
Usage: curl -X PATCH .../v2/accelerator_requests
|
|
-d <patch_list> -H "Content-type: application/json"
|
|
|
|
:param patch_list: A map from ARQ UUIDs to their JSON patches:
|
|
{"$arq_uuid": [
|
|
{"path": "/hostname", "op": ADD/RM, "value": "..."},
|
|
{"path": "/device_rp_uuid", "op": ADD/RM, "value": "..."},
|
|
{"path": "/instance_uuid", "op": ADD/RM, "value": "..."},
|
|
{"path": "/project_id", "op": ADD/RM, "value": "..."},
|
|
],
|
|
"$arq_uuid": [...]
|
|
}
|
|
In particular, all and only these 3 fields must be present,
|
|
and only 'add' or 'remove' ops are allowed.
|
|
"""
|
|
LOG.info('[arqs] patch. list=(%s)', patch_list)
|
|
context = pecan.request.context
|
|
# Validate all patches before un/binding.
|
|
valid_fields = {}
|
|
for arq_uuid, patch in patch_list.items():
|
|
valid_fields[arq_uuid] = self._validate_arq_patch(patch)
|
|
|
|
# NOTE(Sundar): In the ARQ create/bind flow, new ARQs can be created
|
|
# for a device profile any time. However, they should not be bound to
|
|
# an instance which already has other ARQs bound to it. In the future,
|
|
# we may allow that for hot adds, but not now.
|
|
# See commit message of https://review.opendev.org/712231 for details.
|
|
#
|
|
# So, for bind requests, we first check that no ARQs are already
|
|
# associated with the instance specified in the binding.
|
|
patch = list(patch_list.values())[0]
|
|
if patch[0]['op'] == 'add':
|
|
self._check_if_already_bound(context, valid_fields)
|
|
|
|
# TODO(Sundar) Defer to conductor and do all concurently.
|
|
objects.ExtARQ.apply_patch(context, patch_list, valid_fields)
|