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:
@@ -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):
|
||||
|
||||
291
cyborg/api/controllers/v2/arqs.py
Normal file
291
cyborg/api/controllers/v2/arqs.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[:]
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
113
cyborg/tests/unit/api/controllers/v2/test_arqs.py
Normal file
113
cyborg/tests/unit/api/controllers/v2/test_arqs.py
Normal 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)
|
||||
@@ -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():
|
||||
|
||||
68
cyborg/tests/unit/fake_extarq.py
Normal file
68
cyborg/tests/unit/fake_extarq.py
Normal 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
|
||||
Reference in New Issue
Block a user