diff --git a/cyborg/api/controllers/v2/arqs.py b/cyborg/api/controllers/v2/arqs.py index 38fbafb9..9630b4dd 100644 --- a/cyborg/api/controllers/v2/arqs.py +++ b/cyborg/api/controllers/v2/arqs.py @@ -23,6 +23,8 @@ 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 @@ -54,6 +56,8 @@ class ARQ(base.APIBase): 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} @@ -257,6 +261,8 @@ class ARQsController(base.CyborgController): 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( @@ -264,6 +270,12 @@ class ARQsController(base.CyborgController): 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) @@ -306,6 +318,7 @@ class ARQsController(base.CyborgController): {"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": [...] } diff --git a/cyborg/api/controllers/v2/utils.py b/cyborg/api/controllers/v2/utils.py new file mode 100644 index 00000000..613c338d --- /dev/null +++ b/cyborg/api/controllers/v2/utils.py @@ -0,0 +1,22 @@ +# Copyright 2020 Inspur, 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. + +from cyborg import api +from cyborg.api.controllers.v2 import versions + + +def allow_project_id(): + # v2.1 added project_id for arq patch + return api.request.version.minor >= versions.MINOR_1_PROJECT_ID diff --git a/cyborg/api/controllers/v2/versions.py b/cyborg/api/controllers/v2/versions.py index ad52d314..2de475f0 100644 --- a/cyborg/api/controllers/v2/versions.py +++ b/cyborg/api/controllers/v2/versions.py @@ -23,7 +23,9 @@ BASE_VERSION = 2 # explanation of what each version contains. # # v2.0: Initial minor version. +# v2.1: Add project_id for arq patch MINOR_0_INITIAL_VERSION = 0 +MINOR_1_PROJECT_ID = 1 # When adding another version, update: @@ -32,7 +34,7 @@ MINOR_0_INITIAL_VERSION = 0 # explanation of what changed in the new version -MINOR_MAX_VERSION = MINOR_0_INITIAL_VERSION +MINOR_MAX_VERSION = MINOR_1_PROJECT_ID # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_0_INITIAL_VERSION) diff --git a/cyborg/api/rest_api_version_history.rst b/cyborg/api/rest_api_version_history.rst index 970ef6eb..7b56abd0 100644 --- a/cyborg/api/rest_api_version_history.rst +++ b/cyborg/api/rest_api_version_history.rst @@ -12,6 +12,11 @@ user documentation. This is the initial version of the v2 API which supports microversions. +2.1 +--- + +Add ``project_id`` for arq. + A user can specify a header in the API request:: OpenStack-API-Version: accelerator diff --git a/cyborg/common/exception.py b/cyborg/common/exception.py index e368db45..567115c7 100644 --- a/cyborg/common/exception.py +++ b/cyborg/common/exception.py @@ -411,3 +411,8 @@ class InvalidType(Invalid): class ResourceNotFound(NotFound): _msg_fmt = _("%(resource)s not found %(msg)s") + + +class NotAcceptable(CyborgException): + _msg_fmt = _("Request not acceptable.") + code = http_client.NOT_ACCEPTABLE diff --git a/cyborg/objects/arq.py b/cyborg/objects/arq.py index e4d6dc6b..033039ad 100644 --- a/cyborg/objects/arq.py +++ b/cyborg/objects/arq.py @@ -44,6 +44,7 @@ class ARQ(base.CyborgObject, object_base.VersionedObjectDictCompat): 'hostname': object_fields.StringField(nullable=True), 'device_rp_uuid': object_fields.StringField(nullable=True), 'instance_uuid': object_fields.StringField(nullable=True), + 'project_id': object_fields.StringField(nullable=True), # Fields populated by Cyborg after binding 'attach_handle_type': object_fields.StringField(nullable=True), @@ -66,6 +67,6 @@ class ARQ(base.CyborgObject, object_base.VersionedObjectDictCompat): db_extarq['attach_handle_info'] = {} for field in arq.fields: - arq[field] = db_extarq[field] + arq[field] = db_extarq.get(field) arq.obj_reset_changes() return arq diff --git a/cyborg/objects/ext_arq.py b/cyborg/objects/ext_arq.py index 332b22ec..681962de 100644 --- a/cyborg/objects/ext_arq.py +++ b/cyborg/objects/ext_arq.py @@ -278,7 +278,7 @@ class ExtARQ(base.CyborgObject, object_base.VersionedObjectDictCompat, for field in extarq.fields: if field != 'arq': - extarq[field] = db_extarq[field] + extarq[field] = db_extarq.get(field) extarq.arq = objects.ARQ() extarq.arq._from_db_object(extarq.arq, db_extarq) extarq.obj_reset_changes() diff --git a/cyborg/objects/extarq/ext_arq_job.py b/cyborg/objects/extarq/ext_arq_job.py index cb211401..75736443 100644 --- a/cyborg/objects/extarq/ext_arq_job.py +++ b/cyborg/objects/extarq/ext_arq_job.py @@ -65,12 +65,17 @@ class ExtARQJobMixin(object): hostname = valid_fields[self.arq.uuid]['hostname'] devrp_uuid = valid_fields[self.arq.uuid]['device_rp_uuid'] instance_uuid = valid_fields[self.arq.uuid]['instance_uuid'] - LOG.info('[arqs:objs] bind. hostname: %s, devrp_uuid: %s' - 'instance: %s', hostname, devrp_uuid, instance_uuid) + project_id = valid_fields[self.arq.uuid].get('project_id') + LOG.info('[arqs:objs] bind. hostname: %(hostname)s,' + ' devrp_uuid: %(devrp_uuid)s, instance: %(instance)s, ' + 'project_id: %(project_id)s', + {'hostname': hostname, 'devrp_uuid': devrp_uuid, + 'instance_uuid': instance_uuid, 'project_id': project_id}) self.arq.hostname = hostname self.arq.device_rp_uuid = devrp_uuid self.arq.instance_uuid = instance_uuid + self.arq.project_id = project_id # If prog fails, we'll change this ARQ state changes get committed here self.update_check_state(context, constants.ARQ_BIND_STARTED) diff --git a/cyborg/tests/unit/api/controllers/v2/test_arqs.py b/cyborg/tests/unit/api/controllers/v2/test_arqs.py index ba45371a..092fa2e5 100644 --- a/cyborg/tests/unit/api/controllers/v2/test_arqs.py +++ b/cyborg/tests/unit/api/controllers/v2/test_arqs.py @@ -19,6 +19,7 @@ from unittest import mock from oslo_serialization import jsonutils +from cyborg.api.controllers import base from cyborg.api.controllers.v2 import arqs from cyborg.common import exception from cyborg.tests.unit.api.controllers.v2 import base as v2_test @@ -255,13 +256,14 @@ class TestARQsController(v2_test.APITestV2): @mock.patch('cyborg.objects.ExtARQ.apply_patch') def test_apply_patch(self, mock_apply_patch, mock_check_if_bound): """Test the happy path.""" - patch_list = fake_extarq.get_patch_list() + patch_list, device_rp_uuid = fake_extarq.get_patch_list() arq_uuids = list(patch_list.keys()) + obj_extarq = self.fake_extarqs[0] valid_fields = { arq_uuid: { - 'hostname': 'myhost', - 'device_rp_uuid': 'fb16c293-5739-4c84-8590-926f9ab16669', - 'instance_uuid': '5922a70f-1e06-4cfd-88dd-a332120d7144'} + 'hostname': obj_extarq.arq.hostname, + 'device_rp_uuid': device_rp_uuid, + 'instance_uuid': obj_extarq.arq.instance_uuid} for arq_uuid in arq_uuids} self.patch_json(self.ARQ_URL, params=patch_list, @@ -271,6 +273,41 @@ class TestARQsController(v2_test.APITestV2): valid_fields) mock_check_if_bound.assert_called_once_with(mock.ANY, valid_fields) + @mock.patch.object(arqs.ARQsController, '_check_if_already_bound') + @mock.patch('cyborg.objects.ExtARQ.apply_patch') + def test_apply_patch_allow_project_id( + self, mock_apply_patch, mock_check_if_bound): + patch_list, _ = fake_extarq.get_patch_list() + for arq_uuid, patch in patch_list.items(): + patch.append({'path': '/project_id', 'op': 'add', + 'value': 'b1c76756ac2e482789a8e1c5f4bf065e'}) + arq_uuids = list(patch_list.keys()) + valid_fields = { + arq_uuid: { + 'hostname': 'myhost', + 'device_rp_uuid': 'fb16c293-5739-4c84-8590-926f9ab16669', + 'instance_uuid': '5922a70f-1e06-4cfd-88dd-a332120d7144', + 'project_id': 'b1c76756ac2e482789a8e1c5f4bf065e'} + for arq_uuid in arq_uuids} + + self.patch_json(self.ARQ_URL, params=patch_list, + headers={base.Version.current_api_version: + 'accelerator 2.1'}) + mock_apply_patch.assert_called_once_with(mock.ANY, patch_list, + valid_fields) + mock_check_if_bound.assert_called_once_with(mock.ANY, valid_fields) + + def test_apply_patch_not_allow_project_id(self): + patch_list, _ = fake_extarq.get_patch_list() + for arq_uuid, patch in patch_list.items(): + patch.append({'path': '/project_id', 'op': 'add', + 'value': 'b1c76756ac2e482789a8e1c5f4bf065e'}) + response = self.patch_json(self.ARQ_URL, params=patch_list, + headers=self.headers, + expect_errors=True) + self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code) + self.assertTrue(response.json['error_message']) + # TODO(all): Add exception test cases for apply_patch. @mock.patch('cyborg.objects.ExtARQ.list') diff --git a/cyborg/tests/unit/fake_extarq.py b/cyborg/tests/unit/fake_extarq.py index 6067a488..d51ca4c2 100644 --- a/cyborg/tests/unit/fake_extarq.py +++ b/cyborg/tests/unit/fake_extarq.py @@ -288,7 +288,8 @@ def get_patch_list(same_device=True): must be for the same device. """ arqs = _get_arqs_as_dict() - host_binding = {'path': '/hostname', 'op': 'add', 'value': 'myhost'} + host_binding = {'path': '/hostname', 'op': 'add', + 'value': arqs[0]['hostname']} inst_binding = {'path': '/instance_uuid', 'op': 'add', 'value': arqs[0]['instance_uuid']} device_rp_uuid = 'fb16c293-5739-4c84-8590-926f9ab16669' @@ -298,4 +299,4 @@ def get_patch_list(same_device=True): dev_binding = {'path': '/device_rp_uuid', 'op': 'add', 'value': dev_uuid} patch_list[newarq['uuid']] = [host_binding, inst_binding, dev_binding] - return patch_list + return patch_list, device_rp_uuid