Fix api-ref inaccuracies and add missing program endpoint

The ARQ PATCH endpoint was documented with a UUID in the URL path
(PATCH /v2/accelerator_requests/{uuid}) but the code takes no path
UUID — ARQ UUIDs are keys in the JSON request body. The docs also
referenced a response sample, but the endpoint returns 202 with no
body. Fix the URL, remove the orphan response sample, and add the
standard "no body content" text. Add prose clarifying the JSON-Patch
request structure for the parameter table.

The deployables program endpoint (PATCH /v2/deployables/{uuid}/program)
existed in code but had no api-ref documentation. Add the endpoint
section, request/response parameter docs, and sample JSON files.
Document the JSON-Patch envelope in the parameter description and
set bitstream_id to null in the response sample to match what the
controller actually returns.

Add a functional test for the new program endpoint samples, extending
_check_sample() to support PATCH requests with a request body.

Closes-Bug: #2158759
Assisted-by: Claude claude-opus-4-6 claude-code-2.1.196
Change-Id: Iaf9e68cf7174bf9993cef034e07d78f532d1077a
Signed-off-by: melanie witt <melwittt@gmail.com>
This commit is contained in:
melanie witt
2026-07-07 22:38:47 -07:00
parent 18ef09c549
commit 8c9cfb527d
9 changed files with 210 additions and 33 deletions
+15 -5
View File
@@ -75,15 +75,22 @@ to bind and unbind the ARQ with the host name, devices RP UUID and instance U
is an asynchronous call which prepares or reconfigures the device in the
background.
.. rest_method:: PATCH /v2/accelerator_requests/{accelerator_request_uuid}
.. rest_method:: PATCH /v2/accelerator_requests
Updates:an accelerator request. The payload should have these fields:
Bind or unbind one or more accelerator requests. The payload is a JSON
object whose keys are ARQ UUIDs and whose values are lists of JSON-Patch
operations. Only ``add`` (bind) and ``remove`` (unbind) operations are
supported. All operations in a single request must use the same op.
Request
=======
The request body is a JSON object keyed by ARQ UUID. Each value is a
list of JSON-Patch operations whose ``path`` and ``value`` fields carry
the bind/unbind parameters:
.. rest_parameters:: parameters.yaml
- accelerator_request_uuid: accelerator_request_uuid
- hostname: hostname_bind_unbind_req
- device_rp_uuid: device_rp_uuid_bind_unbind_req
- instance_uuid: instance_uuid_bind_unbind_req
@@ -94,9 +101,12 @@ Request
.. literalinclude:: ../../doc/api_samples/accelerator_requests/accelerator_requests-patch-req.json
:language: javascript
**Example response: update an accelerator request**
Response
========
.. literalinclude:: ../../doc/api_samples/accelerator_requests/accelerator_requests-after-update-resp.json
Normal response codes: 202
There is no body content for the response of a successful request.
Delete Accelerator Requests by ARQ uuid
---------------------------------------
+51
View File
@@ -89,3 +89,54 @@ Response
.. literalinclude:: ../../doc/api_samples/deployables/deployables-getone-resp.json
:language: javascript
Program a Deployable
--------------------
.. rest_method:: PATCH /v2/deployables/{deployable_uuid}/program
Program an FPGA deployable with a new bitstream image. This is an
asynchronous operation that reconfigures the FPGA device.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), notfound(404)
Request
=======
The request body is a JSON-Patch array. The ``image_uuid`` is supplied
inside a ``replace`` operation on ``/program``:
.. rest_parameters:: parameters.yaml
- deployable_uuid: deployable_uuid
- image_uuid: image_uuid
**Example request: program a deployable**
.. literalinclude:: ../../doc/api_samples/deployables/deployables-program-req.json
:language: javascript
Response
========
.. rest_parameters:: parameters.yaml
- uuid: deployable_uuid_resp
- parent_id: deployable_parent_id
- root_id: deployable_root_id
- name: deployable_name
- num_accelerators: deployable_num_accelerators
- device_id: deployable_device_id
- attributes_list: deployable_attributes_list
- rp_uuid: deployable_rp_uuid
- driver_name: deployable_driver_name
- bitstream_id: deployable_bitstream_id
- created_at: created
- updated_at: updated
- links: links
**Example response: program a deployable**
.. literalinclude:: ../../doc/api_samples/deployables/deployables-program-resp.json
:language: javascript
+8
View File
@@ -359,6 +359,14 @@ hostname_resp:
in: body
required: true
type: string
image_uuid:
description: |
The UUID of the bitstream image to program onto the FPGA.
Supplied inside a JSON-Patch ``replace`` operation on ``/program``
(see the program endpoint example).
in: body
required: true
type: string
instance_project_id_bind_unbind_req:
description: |
Project id of the target instance for bind or unbind of one accelerator request.
@@ -157,5 +157,24 @@ class TestAcceleratorRequestSamples(base.ApiSampleTestBase):
)
class TestDeployableProgramSample(base.ApiSampleTestBase):
def test_deployable_program(self):
uuids = self.seed_programable_deployable()
self._check_sample(
f'/v2/deployables/{uuids["deployable"]}/program',
os.path.join(
SAMPLES_DIR,
'deployables',
'deployables-program-resp.json',
),
method='PATCH',
req_path=os.path.join(
SAMPLES_DIR,
'deployables',
'deployables-program-req.json',
),
)
if __name__ == '__main__':
unittest.main()
@@ -77,6 +77,16 @@ class ApiSampleTestBase(base.TestCase):
result = common.seed_device_profiles(self.context)
return {k: v.uuid for k, v in result.items()}
def seed_programable_deployable(self):
self.useFixture(
fixtures.MockPatch(
'cyborg.api.controllers.v2.deployables.AgentAPI',
autospec=True,
)
)
result = common.seed_programable_deployable(self.context)
return {k: v.uuid for k, v in result.items()}
def seed_arqs(self):
self.useFixture(
fixtures.MockPatch(
@@ -102,14 +112,36 @@ class ApiSampleTestBase(base.TestCase):
headers.update(extra_headers)
return headers
def _check_sample(self, url, sample_path, extra_headers=None):
def _check_sample(
self, url, sample_path, extra_headers=None, method='GET', req_path=None
):
"""Compare API response structure against a sample JSON file.
When GENERATE_SAMPLES=1, overwrites the sample file instead.
:param method: HTTP method (default GET). For PATCH, also pass
*req_path* with the path to a JSON request body sample.
:param req_path: Path to a JSON file used as the request body
for non-GET methods.
"""
response = self.app.get(
url, headers=self._get_headers(extra_headers), expect_errors=False
)
headers = self._get_headers(extra_headers)
if method == 'PATCH':
if not req_path:
raise ValueError("req_path is required for PATCH requests")
with open(req_path) as f:
body = json.load(f)
response = self.app.patch_json(
url,
body,
headers=headers,
expect_errors=False,
)
else:
response = self.app.get(
url,
headers=headers,
expect_errors=False,
)
actual = response.json
if GENERATE_SAMPLES:
+46
View File
@@ -91,6 +91,52 @@ def seed_devices(context):
}
def seed_programable_deployable(context):
"""Create a device, deployable, and controlpath ID for programming.
The program endpoint requires a controlpath ID associated with the
device so it can locate the FPGA control path.
"""
dev = objects.Device(
context,
uuid=uuidutils.generate_uuid(),
type='FPGA',
vendor='0xABCD',
model='miss model info',
std_board_info="{'device_id': '0xabcd', 'class': 'Fake class'}",
vendor_board_info='fake_vendor_info',
hostname='test-node-1',
status='enabled',
)
dev.create(context)
dep = objects.Deployable(
context,
uuid=uuidutils.generate_uuid(),
name='test-deployable-0',
num_accelerators=1,
device_id=dev.id,
rp_uuid=uuidutils.generate_uuid(),
driver_name='fake',
)
dep.create(context)
cpid = objects.ControlpathID(
context,
uuid=uuidutils.generate_uuid(),
device_id=dev.id,
cpid_type='PCI',
cpid_info='{"domain":"0000","bus":"0c","device":"00","function":"0"}',
)
cpid.create(context)
return {
'device': dev,
'deployable': dep,
'controlpath_id': cpid,
}
def seed_device_profiles(context):
"""Create two test device profiles.
@@ -1,24 +0,0 @@
{
"instance_uuid":"506220bc-18f2-4294-9b29-80267a021dc0",
"hostname":"centos-80",
"uuid":"89ec9c4d-73b2-4aaf-9225-485945aa7162",
"links":[
{
"href":"http://192.168.23.138/accelerator/v2/accelerator_requests/89ec9c4d-73b2-4aaf-9225-485945aa7162",
"rel":"self"
}
],
"created_at":"2019-10-25T12:01:05+00:00",
"device_profile_group_id":0,
"attach_handle_type":"TEST_PCI",
"updated_at":"2019-10-25T12:49:49+00:00",
"state":"Bound",
"device_rp_uuid":"03631f82-20a9-3f67-a29d-dc1abe4042bf",
"device_profile_name":"device-profile-name1",
"attach_handle_info":{
"device":"00",
"bus":"0c",
"domain":"0000",
"function":"0"
}
}
@@ -0,0 +1,11 @@
[
{
"path": "/program",
"value": [
{
"image_uuid": "76babc1b-3222-4992-a046-52441e403577"
}
],
"op": "replace"
}
]
@@ -0,0 +1,24 @@
{
"uuid": "29e23349-12ee-4978-963c-11484a4ae601",
"parent_id": null,
"root_id": null,
"name": "computenode_FakeDevice",
"num_accelerators": 16,
"device_id": 1,
"attributes_list": "[{'traits1': 'CUSTOM_FAKE_DEVICE'}, {'rc': 'FPGA'}]",
"rp_uuid": "853f07a6-19de-3dd6-b9f6-6c782daa3f7b",
"driver_name": "fake",
"bitstream_id": null,
"created_at": "2020-03-13T02:27:35+00:00",
"updated_at": "2020-03-13T02:27:36+00:00",
"links": [
{
"href": "http://localhost/accelerator/v2/deployables/29e23349-12ee-4978-963c-11484a4ae601",
"rel": "self"
},
{
"href": "http://localhost/accelerator/deployables/29e23349-12ee-4978-963c-11484a4ae601",
"rel": "bookmark"
}
]
}