P7: Changes for ARQs and ExtARQs.

Changes to create, get, bind, unbind and delete Accelerator Requests
(ARQs) along with their associated Extended Accelerator Requests
(extARQs). Each ExtARQ has 1:1 relationship with an ARQ and shares its
uuid.

Binding may involve device preparation. For FPGAs, this includes
programming. Accordingly, this change includes Glance interaction
to query and fetch bitstreams.

After all ARQs for a device profile have either bound successfully
or failed to bind, Nova must be notified. That is addressed elsewhere
in https://review.opendev.org/674520.

Change-Id: I03710f78c876ebe587d6619deff175374f5ac4ba
This commit is contained in:
Sundar Nadathur
2019-07-12 00:31:16 -07:00
parent 7175264196
commit aea02c1fc9
9 changed files with 803 additions and 52 deletions

View File

@@ -22,6 +22,7 @@ from wsme import types as wtypes
from cyborg.api.controllers import base
from cyborg.api.controllers import link
from cyborg.api.controllers.v2 import api_version_request
from cyborg.api.controllers.v2 import arqs
from cyborg.api.controllers.v2 import device_profiles
from cyborg.api import expose
@@ -61,9 +62,8 @@ class V2(base.APIBase):
class Controller(rest.RestController):
"""Version 2 API controller root"""
# Enabled in later patches.
device_profiles = device_profiles.DeviceProfilesController()
# accelerator_requests = arqs.ARQsController()
accelerator_requests = arqs.ARQsController()
@expose.expose(V2)
def get(self):

View File

@@ -0,0 +1,291 @@
# 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 import expose
from cyborg.common import exception
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"""
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(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.DeviceProfileNameNotFound(name=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")
@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)
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 instance is not None:
new_arqs = [arq for arq in arqs
if arq['instance_uuid'] == instance]
arqs = new_arqs
if bind_state is not None:
if bind_state != 'resolved':
raise exception.ARQInvalidState(state=bind_state)
unbound_flag = False
for arq in arqs:
if (arq['state'] != 'Bound' and
arq['state'] != 'BindFailed'):
unbound_flag = True
if instance is not None and unbound_flag:
# Return HTTP code 'Locked'
# TODO(Sundar) This should return HTTP code 423
# if any ARQ for this instance is not resolved.
LOG.warning('HTTP Response should be 423')
pecan.response.status = http_client.LOCKED
return None
ret = ARQCollection.convert_with_links(arqs)
LOG.info('[arqs:get_all] Returned: %s', ret)
return ret
# @policy.authorize_wsgi("cyborg:arq", "delete")
@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
: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 ((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 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
# @policy.authorize_wsgi("cyborg:arq", "update")
@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": "..."},
],
"$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)
# TODO(Sundar) Defer to conductor and do all concurently.
objects.ExtARQ.apply_patch(context, patch_list, valid_fields)

View File

@@ -14,6 +14,7 @@
# under the License.
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_versionedobjects import base as object_base
from cyborg.db import api as dbapi
@@ -28,20 +29,26 @@ LOG = logging.getLogger(__name__)
@base.CyborgObjectRegistry.register
class ARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
# 1.1: v2 API and Nova integration
VERSION = '1.1'
dbapi = dbapi.get_instance()
fields = {
'id': object_fields.IntegerField(nullable=False),
'uuid': object_fields.UUIDField(nullable=False),
'state': object_fields.ARQStateField(nullable=False),
'device_profile': object_fields.ObjectField('DeviceProfile',
nullable=True),
'device_profile_name': object_fields.StringField(nullable=False),
'device_profile_group_id':
object_fields.IntegerField(nullable=False),
# Fields populated by Nova after scheduling for binding
'hostname': object_fields.StringField(nullable=True),
'device_rp_uuid': object_fields.UUIDField(nullable=True),
'device_instance_uuid': object_fields.UUIDField(nullable=True),
'attach_handle': object_fields.ObjectField('AttachHandle',
nullable=True),
'device_rp_uuid': object_fields.StringField(nullable=True),
'instance_uuid': object_fields.StringField(nullable=True),
# Fields populated by Cyborg after binding
'attach_handle_type': object_fields.StringField(nullable=True),
'attach_handle_info': object_fields.DictOfStringsField(nullable=True),
}
@staticmethod
@@ -52,23 +59,14 @@ class ARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
:param db_extarq: A DB model of the object
:return: The object of the class with the database entity added
"""
device_profile_id = db_extarq.pop('device_profile_id', None)
attach_handle_id = db_extarq.pop('attach_handle_id', None)
ahi = db_extarq['attach_handle_info']
if ahi is not None and ahi != '':
d = jsonutils.loads(ahi)
db_extarq['attach_handle_info'] = d
else:
db_extarq['attach_handle_info'] = {}
for field in arq.fields:
# if field == 'device_profile':
# arq._load_device_profile(device_profile_id)
# if field == 'attach_handle':
# arq._load_device_profile(attach_handle_id)
arq[field] = db_extarq[field]
arq.obj_reset_changes()
return arq
def _load_device_profile(self, device_profile_id):
self.device_profile = objects.DeviceProfile.\
get_by_id(self._context, device_profile_id)
def _load_attach_handle(self, attach_handle_id):
self.attach_handle = objects.AttachHandle.\
get_by_id(self._context, attach_handle_id)

View File

@@ -108,3 +108,13 @@ class AttachHandle(base.CyborgObject, object_base.VersionedObjectDictCompat):
return ah_obj_list[0]
else:
return None
@classmethod
def allocate(cls, context, deployable_id):
db_ah = cls.dbapi.attach_handle_allocate(context, deployable_id)
obj_ah = cls._from_db_object(cls(context), db_ah)
return obj_ah
def deallocate(self, context):
values = {"in_use": False}
self.dbapi.attach_handle_update(context, self.uuid, values)

View File

@@ -27,8 +27,9 @@ LOG = logging.getLogger(__name__)
@base.CyborgObjectRegistry.register
class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
VERSION = '2.0'
# 1.0: Initial version
# 1.1: Added rp_uuid, driver_name, bitstream_id, num_accel_in_use
VERSION = '1.1'
dbapi = dbapi.get_instance()
attributes_list = []
@@ -46,8 +47,11 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat):
# number of accelerators spawned by this deployable
'device_id': object_fields.IntegerField(nullable=False),
# Foreign key constrain to reference device table
'driver_name': object_fields.StringField(nullable=True)
'driver_name': object_fields.StringField(nullable=True),
# Will change it to non-nullable after other driver report it.
'rp_uuid': object_fields.UUIDField(nullable=True),
# UUID of the Resource provider corresponding to this deployable
'bitstream_id': object_fields.UUIDField(nullable=True),
}
def _get_parent_root_id(self, context):
@@ -89,6 +93,12 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat):
obj_dep.obj_reset_changes()
return obj_dep
@classmethod
def get_by_device_rp_uuid(cls, context, devrp_uuid):
db_dep = cls.dbapi.deployable_get_by_rp_uuid(context, devrp_uuid)
obj_dep = cls._from_db_object(cls(context), db_dep)
return obj_dep
@classmethod
def list(cls, context, filters={}):
"""Return a list of Deployable objects."""
@@ -129,6 +139,11 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat):
query)
self.attributes_list = attr_get_list
def update(self, context, updates):
"""Update provided key, value pairs"""
db_dep = self.dbapi.deployable_update(context, self.uuid,
updates)
def destroy(self, context):
"""Delete a Deployable from the DB."""
del self.attributes_list[:]

View File

@@ -13,17 +13,23 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import connection
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_versionedobjects import base as object_base
from cyborg.agent.rpcapi import AgentAPI
from cyborg.db import api as dbapi
from cyborg import objects
from cyborg.common import constants
from cyborg.common import exception
from cyborg.common import placement_client
from cyborg import objects
from cyborg.objects import base
from cyborg.objects.attach_handle import AttachHandle
from cyborg.objects.deployable import Deployable
from cyborg.objects.device_profile import DeviceProfile
from cyborg.objects import fields as object_fields
LOG = logging.getLogger(__name__)
@@ -38,7 +44,8 @@ class ExtARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
or if the ARQ version changes.
"""
# Version 1.0: Initial version
VERSION = '1.0'
# 1.1: v2 API and Nova integration
VERSION = '1.1'
dbapi = dbapi.get_instance()
@@ -47,17 +54,26 @@ class ExtARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
# Cyborg-private fields
# Left substate open now, fill them out during design/implementation
# later.
'substate': object_fields.StringField(nullable=True),
'substate': object_fields.StringField(),
'deployable_uuid': object_fields.UUIDField(nullable=True),
# The dp group is copied in to the extarq, so that any changes or
# deletions to the device profile do not affect running VMs.
'device_profile_group': object_fields.DictOfStringsField(
nullable=True),
# For bound ARQs, we keep the attach handle ID here so that
# it is easy to deallocate on unbind or delete.
'attach_handle_id': object_fields.IntegerField(nullable=True),
}
def create(self, context, device_profile_id=None):
"""Create an ExtARQ record in the DB."""
if 'device_profile' not in self.arq and not device_profile_id:
if 'device_profile_name' not in self.arq and not device_profile_id:
raise exception.ObjectActionError(
action='create',
reason='Device profile is required in ARQ')
self.arq.state = constants.ARQINITIAL
self.substate = constants.ARQINITIAL
reason='Device profile name is required in ARQ')
self.arq.state = constants.ARQ_INITIAL
self.substate = constants.ARQ_INITIAL
values = self.obj_get_changes()
arq_obj = values.pop('arq', None)
if arq_obj is not None:
@@ -68,24 +84,24 @@ class ExtARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
values['device_profile_id'] = device_profile_id
db_extarq = self.dbapi.extarq_create(context, values)
self._from_db_object(self, db_extarq)
self._from_db_object(self, db_extarq, context)
return self
@classmethod
def get(cls, context, uuid):
"""Find a DB ExtARQ and return an Obj ExtARQ."""
# TODO Fix warnings that '' is not an UUID
db_extarq = cls.dbapi.extarq_get(context, uuid)
obj_arq = objects.ARQ(context)
obj_extarq = ExtARQ(context)
obj_extarq['arq'] = obj_arq
obj_extarq = cls._from_db_object(obj_extarq, db_extarq)
obj_extarq = cls._from_db_object(obj_extarq, db_extarq, context)
return obj_extarq
@classmethod
def list(cls, context, limit, marker, sort_key, sort_dir):
def list(cls, context):
"""Return a list of ExtARQ objects."""
db_extarqs = cls.dbapi.extarq_list(context, limit, marker, sort_key,
sort_dir)
db_extarqs = cls.dbapi.extarq_list(context)
obj_extarq_list = cls._from_db_object_list(db_extarqs, context)
return obj_extarq_list
@@ -93,44 +109,272 @@ class ExtARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
"""Update an ExtARQ record in the DB."""
updates = self.obj_get_changes()
db_extarq = self.dbapi.extarq_update(context, self.arq.uuid, updates)
self._from_db_object(self, db_extarq)
self._from_db_object(self, db_extarq, context)
def destroy(self, context):
"""Delete an ExtARQ from the DB."""
self.dbapi.extarq_delete(context, self.arq.uuid)
self.obj_reset_changes()
def bind(self, context, host_name, devrp_uuid, instance_uuid):
@classmethod
def delete_by_uuid(cls, context, arq_uuid_list):
for uuid in arq_uuid_list:
obj_extarq = objects.ExtARQ.get(context, uuid)
# TODO Defer deletion to conductor
if obj_extarq.arq.state != constants.ARQ_STATE_INITIAL:
obj_extarq.unbind(context)
obj_extarq.destroy(context)
@classmethod
def delete_by_instance(cls, context, instance_uuid):
"""Delete all ARQs for given instance."""
obj_extarqs = [extarq for extarq in objects.ExtARQ.list(context)
if extarq.arq['instance_uuid'] == instance_uuid]
for obj_extarq in obj_extarqs:
LOG.info('Deleting obj_extarq uuid %s for instance %s',
obj_extarq.arq['uuid'], obj_extarq.arq['instance_uuid'])
obj_extarq.unbind(context)
obj_extarq.destroy(context)
def _get_glance_connection(self):
default_user = 'devstack-admin'
try:
auth_user = CONF.image.username or default_user
except:
auth_user = default_user
return connection.Connection(cloud=auth_user)
def _get_bitstream_md_from_function_id(self, function_id):
"""Get bitstream metadata given a function id."""
conn = self._get_glance_connection()
properties = {'accel:function_id': function_id}
resp = conn.image.get('/images', params=properties)
if resp:
image_list = resp.json()['images']
if type(image_list) != list:
raise exception.InvalidType(
obj='image', type=type(image_list),
expected='list')
if len(image_list) != 1:
raise exception.ExpectedOneObject(obj='image',
count=len(image_list))
return image_list[0]
else:
LOG.warning('Failed to get image for function (%s)',
function_id)
return None
def _get_bitstream_md_from_bitstream_id(self, bitstream_id):
"""Get bitstream metadata given a bitstream id."""
conn = self._get_glance_connection()
resp = conn.image.get('/images/' + bitstream_id)
if resp:
return resp.json()
else:
LOG.warning('Failed to get image for bitstream (%s)',
bitstream_id)
return None
def _do_programming(self, context, hostname,
deployable, bitstream_id):
driver_name = deployable.driver_name
query_filter = {"device_id": deployable.device_id}
# TODO We should probably get cpid from objects layer, not db layer
cpid_list = self.dbapi.control_path_get_by_filters(
context, query_filter)
count = len(cpid_list)
if count != 1:
raise exception.ExpectedOneObject(type='controlpath_id',
count=count)
controlpath_id = cpid_list[0]
controlpath_id['cpid_info'] = jsonutils.loads(
controlpath_id['cpid_info'])
LOG.info('Found control path id: %s', controlpath_id.__dict__)
LOG.info('Starting programming for host: (%s) deployable (%s) '
'bitstream_id (%s)', hostname,
deployable.uuid, bitstream_id)
agent = AgentAPI()
# TODO do this asynchronously
# TODO do this in the conductor
agent.fpga_program_v2(context, hostname,
controlpath_id, bitstream_id,
driver_name)
LOG.info('Finished programming for host: (%s) deployable (%s)',
hostname, deployable.uuid)
# TODO propagate agent errors to caller
return True
def _update_placement(self, devrp_uuid, function_id, bitstream_md):
placement = placement_client.PlacementClient()
placement.delete_traits_with_prefixes(
devrp_uuid, ['CUSTOM_FPGA_FUNCTION_ID'])
# TODO(Sundar) Don't apply function trait if bitstream is private
if not function_id:
function_id = bitstream_md.get('accel:function_id')
if function_id:
function_id = function_id.upper().replace('-', '_-')
# TODO(Sundar) Validate this is a valid trait name
# TODO(Sundar) Add vendor name to trait to match spec
trait_names = ['CUSTOM_FPGA_FUNCTION_ID_' + function_id]
placement.add_traits_to_rp(devrp_uuid, trait_names)
def bind(self, context, hostname, devrp_uuid, instance_uuid):
""" Given a device rp UUID, get the deployable UUID and
an attach handle.
"""
# For the fake device, we just set the state to 'Bound'
# TODO(wangzhh): Move bind logic and unbind logic to the agent later.
LOG.info('[arqs:objs] bind. hostname: %s, devrp_uuid: %s'
'instance: %s', hostname, devrp_uuid, instance_uuid)
bitstream_id = self.device_profile_group.get('accel:bitstream_id')
function_id = self.device_profile_group.get('accel:function_id')
programming_needed = (bitstream_id is not None or
function_id is not None)
if (programming_needed and
bitstream_id is not None and function_id is not None):
raise exception.InvalidParameterValue(
'In device profile {0}, only one among bitstream_id '
'and function_id must be set, but both are set')
deployable = Deployable.get_by_device_rp_uuid(context, devrp_uuid)
# TODO Check that deployable.device.hostname matches param hostname
# Note(Sundar): We associate the ARQ with instance UUID before the
# programming starts. So, if programming fails and then Nova calls
# to delete all ARQs for a given instance, we can still pick all
# the relevant ARQs.
arq = self.arq
arq.host_name = host_name
arq.hostname = hostname
arq.device_rp_uuid = devrp_uuid
arq.instance_uuid = instance_uuid
arq.state = constants.ARQBOUND
# If prog fails, we'll change the state
arq.state = constants.ARQ_BIND_STARTED
self.save(context) # ARQ changes get committed here
self.save(context)
if programming_needed:
LOG.info('[arqs:objs] bind. Programming needed. '
'bitstream: (%s) function: (%s) Deployable UUID: (%s)',
bitstream_id or '', function_id or '',
deployable.uuid)
if bitstream_id is not None: # FPGA aaS
bitstream_md = self._get_bitstream_md_from_bitstream_id(
bitstream_id)
else: # Accelerated Function aaS
bitstream_md = self._get_bitstream_md_from_function_id(
function_id)
LOG.info('[arqs:objs] For function id (%s), got '
'bitstream id (%s)', function_id,
bitstream_md['id'])
bitstream_id = bitstream_md['id']
if deployable.bitstream_id == bitstream_id:
LOG.info('Deployable %s already has the needed '
'bitstream %s. Skipping programming.' %
(deployable.uuid, bitstream_id))
else:
ok = self._do_programming(context, hostname,
deployable, bitstream_id)
if ok:
self._update_placement(devrp_uuid, function_id,
bitstream_md)
deployable.update(context, {'bitstream_id': bitstream_id})
arq.state = constants.ARQ_BOUND
else:
arq.state = constants.ARQ_BIND_FAILED
# If programming was done, arq.state already got updated.
# If no programming was needed, transition to BOUND state.
if arq.state == constants.ARQ_BIND_STARTED:
arq.state = constants.ARQ_BOUND
# We allocate attach handle after programming because, if latter
# fails, we need to deallocate the AH
if arq.state == constants.ARQ_BOUND: # still on happy path
try:
ah = AttachHandle.allocate(context, deployable.id)
self.attach_handle_id = ah.id
except:
LOG.error("Failed to allocate attach handle for ARQ %s"
"from deployable %s" % (arq.uuid, deployable.uuid))
arq.state = constants.ARQ_BIND_FAILED
self.arq = arq
self.save(context) # ARQ state changes get committed here
@classmethod
def apply_patch(cls, context, patch_list, valid_fields):
"""Apply JSON patch. See api/controllers/v1/arqs.py. """
for arq_uuid, patch in patch_list.items():
extarq = ExtARQ.get(context, arq_uuid)
if patch[0]['op'] == 'add': # All ops are 'add'
extarq.bind(context,
valid_fields[arq_uuid]['hostname'],
valid_fields[arq_uuid]['device_rp_uuid'],
valid_fields[arq_uuid]['instance_uuid'])
else:
extarq.unbind(context)
def unbind(self, context):
arq = self.arq
arq.host_name = ''
arq.hostname = ''
arq.device_rp_uuid = ''
arq.instance_uuid = ''
arq.state = constants.ARQUNBOUND
arq.state = constants.ARQ_UNBOUND
# Unbind: mark attach handles as freed
ah_id = self.attach_handle_id
if ah_id:
attach_handle = AttachHandle.get_by_id(context, ah_id)
attach_handle.deallocate(context)
self.save(context)
@staticmethod
def _from_db_object(extarq, db_extarq):
@classmethod
def _fill_obj_extarq_fields(cls, context, db_extarq):
""" ExtARQ object has some fields that are not present
in db_extarq. We fill them out here.
"""
# From the 2 fields in the ExtARQ, we obtain other fields.
devprof_id = db_extarq['device_profile_id']
devprof_group_id = db_extarq['device_profile_group_id']
devprof = cls.dbapi.device_profile_get_by_id(context, devprof_id)
db_extarq['device_profile_name'] = devprof['name']
db_extarq['attach_handle_type'] = ''
db_extarq['attach_handle_info'] = ''
if db_extarq['state'] == 'Bound': # TODO Do proper bind
db_ah = cls.dbapi.attach_handle_get_by_id(
context, db_extarq['attach_handle_id'])
if db_ah is not None:
db_extarq['attach_handle_type'] = db_ah['attach_type']
db_extarq['attach_handle_info'] = db_ah['attach_info']
else:
raise exception.ResourceNotFound(
resource='attach handle',
msg='')
# TODO Get the deployable_uuid
db_extarq['deployable_uuid'] = ''
# Get the device profile group
obj_devprof = DeviceProfile.get(context, devprof['name'])
groups = obj_devprof['groups']
db_extarq['device_profile_group'] = groups[devprof_group_id]
return db_extarq
@classmethod
def _from_db_object(cls, extarq, db_extarq, context):
"""Converts an ExtARQ to a formal object.
:param extarq: An object of the class ExtARQ
:param db_extarq: A DB model of the object
:return: The object of the class with the database entity added
"""
cls._fill_obj_extarq_fields(context, db_extarq)
for field in extarq.fields:
if field != 'arq':
extarq[field] = db_extarq[field]
@@ -139,6 +383,16 @@ class ExtARQ(base.CyborgObject, object_base.VersionedObjectDictCompat):
extarq.obj_reset_changes()
return extarq
@classmethod
def _from_db_object_list(cls, db_objs, context):
"""Converts a list of ExtARQs to a list of formal objects."""
objs = []
for db_obj in db_objs:
extarq = cls(context)
obj = cls._from_db_object(extarq, db_obj, context)
objs.append(obj)
return objs
def obj_get_changes(self):
"""Returns a dict of changed fields and their new values."""
changes = {}

View File

@@ -0,0 +1,113 @@
# 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 mock
from six.moves import http_client
from oslo_serialization import jsonutils
from cyborg.common import exception
from cyborg.tests.unit.api.controllers.v2 import base as v2_test
from cyborg.tests.unit import fake_device_profile
from cyborg.tests.unit import fake_extarq
class TestARQsController(v2_test.APITestV2):
ARQ_URL = '/accelerator_requests'
def setUp(self):
super(TestARQsController, self).setUp()
self.headers = self.gen_headers(self.context)
self.fake_extarqs = fake_extarq.get_fake_extarq_objs()
def _validate_links(self, links, arq_uuid):
has_self_link = False
for link in links:
if link['rel'] == 'self':
has_self_link = True
url = link['href']
components = url.split('/')
self.assertEqual(components[-1], arq_uuid)
self.assertTrue(has_self_link)
def _validate_arq(self, in_arq, out_arq):
for field in in_arq.keys():
if field != 'id':
self.assertEqual(in_arq[field], out_arq[field])
# Check that the link is properly set up
self._validate_links(out_arq['links'], in_arq['uuid'])
@mock.patch('cyborg.objects.ExtARQ.get')
def test_get_one_by_uuid(self, mock_extarq):
in_extarq = self.fake_extarqs[0]
in_arq = in_extarq.arq
mock_extarq.return_value = in_extarq
uuid = in_arq['uuid']
url = self.ARQ_URL + '/%s'
out_arq = self.get_json(url % uuid, headers=self.headers)
mock_extarq.assert_called_once()
self._validate_arq(in_arq, out_arq)
@mock.patch('cyborg.objects.ExtARQ.list')
def test_get_all(self, mock_extarqs):
mock_extarqs.return_value = self.fake_extarqs
data = self.get_json(self.ARQ_URL, headers=self.headers)
out_arqs = data['arqs']
self.assertTrue(isinstance(out_arqs, list))
self.assertTrue(len(out_arqs), len(self.fake_extarqs))
for in_extarq, out_arq in zip(self.fake_extarqs, out_arqs):
self._validate_arq(in_extarq.arq, out_arq)
@mock.patch('cyborg.objects.DeviceProfile.get')
@mock.patch('cyborg.objects.ExtARQ.create')
def test_create(self, mock_obj_extarq, mock_obj_dp):
dp_list = fake_device_profile.get_obj_devprofs()
mock_obj_dp.return_value = dp = dp_list[0]
mock_obj_extarq.side_effect = self.fake_extarqs
params = {'device_profile_name': dp['name']}
response = self.post_json(self.ARQ_URL, params, headers=self.headers)
data = jsonutils.loads(response.__dict__['controller_output'])
out_arqs = data['arqs']
self.assertEqual(http_client.CREATED, response.status_int)
self.assertEqual(len(out_arqs), 3)
for in_extarq, out_arq in zip(self.fake_extarqs, out_arqs):
self._validate_arq(in_extarq.arq, out_arq)
for idx, out_arq in enumerate(out_arqs):
dp_group_id = 1
if idx == 0: # 1st arq has group_id '0', other 2 have '1'
dp_group_id = 0
self.assertEqual(dp_group_id, out_arq['device_profile_group_id'])
@mock.patch('cyborg.objects.ExtARQ.delete_by_uuid')
@mock.patch('cyborg.objects.ExtARQ.delete_by_instance')
def test_delete(self, mock_by_inst, mock_by_arq):
url = self.ARQ_URL
arq = self.fake_extarqs[0].arq
instance = arq.instance_uuid
mock_by_arq.return_value = None
args = '?' + "arqs=" + str(arq['uuid'])
response = self.delete(url + args, headers=self.headers)
self.assertEqual(http_client.NO_CONTENT, response.status_int)
mock_by_inst.return_value = None
args = '?' + "instance=" + instance
response = self.delete(url + args, headers=self.headers)
self.assertEqual(http_client.NO_CONTENT, response.status_int)

View File

@@ -31,7 +31,9 @@ def fake_db_deployable(**updates):
'root_id': 1,
'num_accelerators': 4,
'device_id': 0,
'driver_name': "fake-driver-name"
'driver_name': "fake-driver-name",
'rp_uuid': None,
'bitstream_id': None,
}
for name, field in objects.Deployable.fields.items():

View File

@@ -0,0 +1,68 @@
# Copyright 2019 Intel Inc.
#
# 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 cyborg.objects import arq
from cyborg.objects import ext_arq
def _get_arqs_as_dict():
common = {
"state": "Bound",
"device_profile_name": "afaas_example_1",
"hostname": "myhost",
"instance_uuid": "5922a70f-1e06-4cfd-88dd-a332120d7144",
"attach_handle_type": "PCI",
# attach_handle info should vary across ARQs but ignored for testing
"attach_handle_info": {
"bus": "1",
"device": "0",
"domain": "0",
"function": "0"
},
}
arqs = [ # Corresponds to 1st device profile in fake_device)profile.py
{"uuid": "a097fefa-da62-4630-8e8b-424c0e3426dc",
"device_profile_group_id": 0,
"device_rp_uuid": "8787595e-9954-49f8-b5c1-cdb55b59062f",
},
{"uuid": "aa140114-4869-45ea-8213-45f530804b0f",
"device_profile_group_id": 1,
"device_rp_uuid": "a1ec17f2-0051-4737-bac4-f074d8a01a9c",
},
{"uuid": "292b2fa2-0831-484c-aeac-09c794428a5d",
"device_profile_group_id": 1,
"device_rp_uuid": "a1ec17f2-0051-4737-bac4-f074d8a01a9c",
},
]
new_arqs = []
for idx, arq in enumerate(arqs):
arq.update(common)
arq.update(id=idx)
new_arqs.append(arq)
return new_arqs
def _convert_from_dict_to_obj(arq_dict):
obj_arq = arq.ARQ()
for field in arq_dict.keys():
obj_arq[field] = arq_dict[field]
obj_extarq = ext_arq.ExtARQ()
obj_extarq.arq = obj_arq
return obj_extarq
def get_fake_extarq_objs():
arq_list = _get_arqs_as_dict()
obj_extarqs = map(_convert_from_dict_to_obj, arq_list)
return obj_extarqs