diff --git a/cyborg/api/controllers/v2/devices.py b/cyborg/api/controllers/v2/devices.py index 26006526..2eebdbf5 100644 --- a/cyborg/api/controllers/v2/devices.py +++ b/cyborg/api/controllers/v2/devices.py @@ -13,19 +13,25 @@ # License for the specific language governing permissions and limitations # under the License. +from http import HTTPStatus import pecan import wsme from wsme import types as wtypes from oslo_log import log +from cyborg.accelerator.common import exception +from cyborg import api from cyborg.api.controllers import base from cyborg.api.controllers import link from cyborg.api.controllers import types +from cyborg.api.controllers.v2 import versions from cyborg.api import expose from cyborg.common import authorize_wsgi +from cyborg.common import placement_client from cyborg import objects + LOG = log.getLogger(__name__) @@ -56,6 +62,9 @@ class Device(base.APIBase): hostname = wtypes.text """The hostname of the device""" + status = wtypes.text + """The status of the device""" + links = wsme.wsattr([link.Link], readonly=True) """A list containing a self link""" @@ -69,6 +78,9 @@ class Device(base.APIBase): @classmethod def convert_with_links(cls, obj_device): api_device = cls(**obj_device.as_dict()) + # return status filed from microversion of 2.3 + if not api.request.version.minor >= versions.MINOR_3_DEVICE_STATUS: + delattr(api_device, 'status') api_device.links = [ link.Link.make_link('self', pecan.request.public_url, 'devices', api_device.uuid) @@ -93,6 +105,8 @@ class DeviceCollection(base.APIBase): class DevicesController(base.CyborgController): """REST controller for Devices.""" + _custom_actions = {'disable': ['POST'], 'enable': ['POST']} + @authorize_wsgi.authorize_wsgi("cyborg:device", "get_one") @expose.expose(Device, wtypes.text) def get_one(self, uuid): @@ -128,3 +142,51 @@ class DevicesController(base.CyborgController): obj_devices = objects.Device.list(context, filters=filters_dict) LOG.info('[devices:get_all] Returned: %s', obj_devices) return DeviceCollection.convert_with_links(obj_devices) + + @authorize_wsgi.authorize_wsgi("cyborg:device", "disable") + @expose.expose(None, wtypes.text, types.uuid, + status_code=HTTPStatus.OK) + def disable(self, uuid): + context = pecan.request.context + device = objects.Device.get(context, uuid) + device.status = 'maintaining' + device.save(context) + # update resource provider inventories + client = placement_client.PlacementClient() + deployable = objects.Deployable.get_by_id(context, device.id) + filters = {'deployable_id': deployable.id, 'key': 'rc'} + attributes = objects.Attribute.get_by_filter(context, filters) + if attributes: + att_type = attributes[0].value + else: + raise exception.ResourceNotFound( + resource='Attribute', + msg='with deployable_id=%s,key=%s' % (deployable.id, 'rc')) + client.update_rp_inventory_reserved( + deployable.rp_uuid, att_type, + deployable.num_accelerators, + deployable.num_accelerators) + + @authorize_wsgi.authorize_wsgi("cyborg:device", "enable") + @expose.expose(None, wtypes.text, types.uuid, + status_code=HTTPStatus.OK) + def enable(self, uuid): + context = pecan.request.context + device = objects.Device.get(context, uuid) + device.status = 'enabled' + device.save(context) + # update resource provider inventories + client = placement_client.PlacementClient() + deployable = objects.Deployable.get_by_id(context, device.id) + filters = {'deployable_id': deployable.id, 'key': 'rc'} + attributes = objects.Attribute.get_by_filter(context, filters) + if attributes: + att_type = attributes[0].value + else: + raise exception.ResourceNotFound( + resource='Attribute', + msg='with deployable_id=%s,key=%s' % (deployable.id, 'rc')) + client.update_rp_inventory_reserved( + deployable.rp_uuid, att_type, + deployable.num_accelerators, + 0) diff --git a/cyborg/api/controllers/v2/versions.py b/cyborg/api/controllers/v2/versions.py index 6722cc80..490ed19e 100644 --- a/cyborg/api/controllers/v2/versions.py +++ b/cyborg/api/controllers/v2/versions.py @@ -25,10 +25,11 @@ BASE_VERSION = 2 # v2.0: Initial minor version. # v2.1: Add project_id for arq patch # v2.2: Support getting device profile by name (newly introduced) and uuid. +# v2.3: Add status info for device API. MINOR_0_INITIAL_VERSION = 0 MINOR_1_PROJECT_ID = 1 MINOR_2_DP_BY_NAME = 2 - +MINOR_3_DEVICE_STATUS = 3 # When adding another version, update: # - MINOR_MAX_VERSION @@ -36,7 +37,7 @@ MINOR_2_DP_BY_NAME = 2 # explanation of what changed in the new version -MINOR_MAX_VERSION = MINOR_2_DP_BY_NAME +MINOR_MAX_VERSION = MINOR_3_DEVICE_STATUS # 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 498c5170..8fa24e4b 100644 --- a/cyborg/api/rest_api_version_history.rst +++ b/cyborg/api/rest_api_version_history.rst @@ -37,3 +37,11 @@ Changed ``device_profile_uuid`` to ``device_profile_name_or_uuid`` in name (newly introduced) and uuid. - GET /v2/device_profiles/{device_profile_name_or_uuid} + +2.3 +--- + +Add new status info for list Device and get device API. + + - GET: /devices + - GET: /devices/{uuid} diff --git a/cyborg/common/constants.py b/cyborg/common/constants.py index 34bef32b..845b09b2 100644 --- a/cyborg/common/constants.py +++ b/cyborg/common/constants.py @@ -66,6 +66,10 @@ DEVICE_TYPE = (DEVICE_GPU, DEVICE_FPGA, DEVICE_AICHIP, DEVICE_QAT, DEVICE_NIC, DEVICE_SSD) +# Device type +DEVICE_STATUS = ("enabled", "maintaining") + + # Attach handle type # 'TEST_PCI': used by fake driver, ignored by Nova virt driver. ATTACH_HANDLE_TYPES = (AH_TYPE_PCI, AH_TYPE_MDEV, AH_TYPE_TEST_PCI) = ( diff --git a/cyborg/common/placement_client.py b/cyborg/common/placement_client.py index 2eaac2d3..4bb5ff81 100644 --- a/cyborg/common/placement_client.py +++ b/cyborg/common/placement_client.py @@ -148,7 +148,7 @@ class PlacementClient(object): def update_inventory( self, resource_provider_uuid, inventories, - resource_provider_generation=None): + resource_provider_generation=None, version=None): if resource_provider_generation is None: resource_provider_generation = self.get_resource_provider( resource_provider_uuid=resource_provider_uuid)['generation'] @@ -158,7 +158,7 @@ class PlacementClient(object): 'inventories': inventories } try: - return self.put(url, body).json() + return self.put(url, body, version=version).json() except ks_exc.NotFound: raise exception.PlacementResourceProviderNotFound( resource_provider=resource_provider_uuid) @@ -358,3 +358,7 @@ class PlacementClient(object): elif resp.status_code == 204: LOG.info("Successfully delete trait %(trait_name).", { "trait_name", name}) + + def update_rp_inventory_reserved(self, rp_uuid, resource, total, reserved): + update_inventory = {resource: {"total": total, "reserved": reserved}} + self.update_inventory(rp_uuid, update_inventory, version='1.26') diff --git a/cyborg/common/policy.py b/cyborg/common/policy.py index d0798cfb..746bb115 100644 --- a/cyborg/common/policy.py +++ b/cyborg/common/policy.py @@ -48,6 +48,12 @@ device_policies = [ policy.RuleDefault('cyborg:device:get_all', 'rule:allow', description='Retrieve all device records'), + policy.RuleDefault('cyborg:device:disable', + 'rule:admin_api', + description='Disable a device'), + policy.RuleDefault('cyborg:device:enable', + 'rule:admin_api', + description='Enable a device'), ] deployable_policies = [ diff --git a/cyborg/db/sqlalchemy/alembic/versions/6c77bd6afea5_add_device_status.py b/cyborg/db/sqlalchemy/alembic/versions/6c77bd6afea5_add_device_status.py new file mode 100644 index 00000000..ad560a06 --- /dev/null +++ b/cyborg/db/sqlalchemy/alembic/versions/6c77bd6afea5_add_device_status.py @@ -0,0 +1,21 @@ +"""add-device-status + +Revision ID: 6c77bd6afea5 +Revises: 4cc1d79978fc +Create Date: 2023-08-15 23:05:31.918963 + +""" + +# revision identifiers, used by Alembic. +revision = '6c77bd6afea5' +down_revision = '4cc1d79978fc' + +from alembic import op +import sqlalchemy as sa + + + +def upgrade(): + new_column = sa.Column('status', sa.Enum('enabled', 'maintaining'), + nullable=False, default='enabled') + op.add_column('devices', new_column) diff --git a/cyborg/db/sqlalchemy/models.py b/cyborg/db/sqlalchemy/models.py index 1f445f12..7ddbe059 100644 --- a/cyborg/db/sqlalchemy/models.py +++ b/cyborg/db/sqlalchemy/models.py @@ -88,6 +88,8 @@ class Device(Base): std_board_info = Column(Text, nullable=True) vendor_board_info = Column(Text, nullable=True) hostname = Column(String(255), nullable=False) + status = Column(Enum("enabled", "maintaining", name='device_status'), + default='enabled', nullable=False) class Deployable(Base): diff --git a/cyborg/objects/device.py b/cyborg/objects/device.py index a783a24b..3a357a00 100644 --- a/cyborg/objects/device.py +++ b/cyborg/objects/device.py @@ -29,7 +29,8 @@ LOG = logging.getLogger(__name__) class Device(base.CyborgObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Add AICHIP, GENERIC type - VERSION = '1.1' + # Version 1.2: Add status field + VERSION = '1.2' dbapi = dbapi.get_instance() @@ -43,6 +44,8 @@ class Device(base.CyborgObject, object_base.VersionedObjectDictCompat): 'std_board_info': object_fields.StringField(nullable=True), 'vendor_board_info': object_fields.StringField(nullable=True), 'hostname': object_fields.StringField(nullable=False), + 'status': object_fields.EnumField(valid_values=constants.DEVICE_STATUS, + nullable=False, default="enabled"), } def create(self, context):