"Fossies" - the Fresh Open Source Software Archive

Member "openstack-cyborg-9.0.0/cyborg/api/controllers/v2/arqs.py" (5 Oct 2022, 14453 Bytes) of package /linux/misc/openstack/openstack-cyborg-9.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "arqs.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 8.0.0_vs_9.0.0.

    1 # Copyright 2019 Intel, Inc.
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 from http import HTTPStatus
   17 import pecan
   18 import wsme
   19 from wsme import types as wtypes
   20 
   21 from oslo_log import log
   22 
   23 from cyborg.api.controllers import base
   24 from cyborg.api.controllers import link
   25 from cyborg.api.controllers import types
   26 from cyborg.api.controllers.v2 import utils
   27 from cyborg.api.controllers.v2 import versions
   28 from cyborg.api import expose
   29 from cyborg.common import authorize_wsgi
   30 from cyborg.common import constants
   31 from cyborg.common import exception
   32 from cyborg.common.i18n import _
   33 from cyborg import objects
   34 
   35 LOG = log.getLogger(__name__)
   36 
   37 
   38 class ARQ(base.APIBase):
   39     """API representation of an ARQ.
   40 
   41     This class enforces type checking and value constraints, and converts
   42     between the internal object model and the API representation.
   43     """
   44     uuid = types.uuid
   45     """The UUID of the ARQ"""
   46 
   47     state = wtypes.text  # obvious meanings
   48     device_profile_name = wtypes.text
   49     device_profile_group_id = wtypes.IntegerType()
   50 
   51     hostname = wtypes.text
   52     """The host name to which the ARQ is bound, if any"""
   53 
   54     device_rp_uuid = wtypes.text
   55     """The UUID of the bound device RP, if any"""
   56 
   57     instance_uuid = wtypes.text
   58     """The UUID of the instance associated with this ARQ, if any"""
   59     project_id = wtypes.text
   60     """The UUID of the instance project_id associated with this ARQ, if any"""
   61 
   62     attach_handle_type = wtypes.text
   63     attach_handle_info = {wtypes.text: wtypes.text}
   64 
   65     links = wsme.wsattr([link.Link], readonly=True)
   66     """A list containing a self link"""
   67 
   68     def __init__(self, **kwargs):
   69         super(ARQ, self).__init__(**kwargs)
   70         self.fields = []
   71         for field in objects.ARQ.fields:
   72             self.fields.append(field)
   73             setattr(self, field, kwargs.get(field, wtypes.Unset))
   74 
   75     @classmethod
   76     def convert_with_links(cls, obj_arq):
   77         api_arq = cls(**obj_arq.as_dict())
   78         api_arq.links = [
   79             link.Link.make_link('self', pecan.request.public_url,
   80                                 'accelerator_requests', api_arq.uuid)
   81             ]
   82         return api_arq
   83 
   84 
   85 class ARQCollection(base.APIBase):
   86     """API representation of a collection of arqs."""
   87 
   88     arqs = [ARQ]
   89     """A list containing arq objects"""
   90 
   91     @classmethod
   92     def convert_with_links(cls, obj_arqs):
   93         collection = cls()
   94         collection.arqs = [ARQ.convert_with_links(obj_arq)
   95                            for obj_arq in obj_arqs]
   96         return collection
   97 
   98 
   99 class ARQsController(base.CyborgController):
  100     """REST controller for ARQs.
  101 
  102        For the relationship between ARQs and device profiles, see
  103        nova/nova/accelerator/cyborg.py.
  104     """
  105 
  106     @authorize_wsgi.authorize_wsgi("cyborg:arq", "create", False)
  107     @expose.expose(ARQCollection, body=types.jsontype,
  108                    status_code=HTTPStatus.CREATED)
  109     def post(self, req):
  110         """Create one or more ARQs for a single device profile.
  111            Request body:
  112               { 'device_profile_name': <string> }
  113            Future:
  114               { 'device_profile_name': <string> # required
  115                 'device_profile_group_id': <integer>, # opt, default=0
  116                 'image_uuid': <glance-image-UUID>, #optional, for future
  117               }
  118            :param req: request body.
  119         """
  120         LOG.info("[arq] post req = (%s)", req)
  121         context = pecan.request.context
  122         dp_name = req.get('device_profile_name')
  123         if dp_name is not None:
  124             try:
  125                 devprof = objects.DeviceProfile.get_by_name(context, dp_name)
  126             except exception.ResourceNotFound:
  127                 raise exception.ResourceNotFound(
  128                     resource='Device Profile',
  129                     msg='with name=%s' % dp_name)
  130             except Exception as e:
  131                 raise e
  132         else:
  133             raise exception.DeviceProfileNameNeeded()
  134         LOG.info('[arqs] post. device profile name=%s', dp_name)
  135 
  136         extarq_list = []
  137         for group_id, group in enumerate(devprof.groups):
  138             accel_resources = []
  139             # If the device profile requires the Xilinx fpga, the number of
  140             # resources should multiply by 2 cause that end user can program
  141             # the device only when both MGMT and USER PF are bound to
  142             # instance.
  143             if group.get("trait:CUSTOM_FPGA_XILINX") == "required":
  144                 accel_resources = [int(group.get("resources:FPGA"))] * 2
  145             else:
  146                 accel_resources = [
  147                     int(val) for key, val in group.items()
  148                     if key.startswith('resources')]
  149 
  150             # If/when we introduce non-accelerator resources, like
  151             # device-local memory, the key search above needs to be
  152             # made specific to accelerator resources only.
  153             num_accels = sum(accel_resources)
  154             arq_fields = {
  155                 'device_profile_name': devprof.name,
  156                 'device_profile_group_id': group_id,
  157             }
  158             for i in range(num_accels):
  159                 obj_arq = objects.ARQ(context, **arq_fields)
  160                 extarq_fields = {'arq': obj_arq}
  161                 obj_extarq = objects.ExtARQ(context, **extarq_fields)
  162                 new_extarq = pecan.request.conductor_api.arq_create(
  163                     context, obj_extarq, devprof.id)
  164                 extarq_list.append(new_extarq)
  165 
  166         ret = ARQCollection.convert_with_links(
  167             [extarq.arq for extarq in extarq_list])
  168         LOG.info('[arqs] post returned: %s', ret)
  169         return ret
  170 
  171     @authorize_wsgi.authorize_wsgi("cyborg:arq", "get_one")
  172     @expose.expose(ARQ, wtypes.text)
  173     def get_one(self, uuid):
  174         """Get a single ARQ by UUID."""
  175         context = pecan.request.context
  176         extarq = objects.ExtARQ.get(context, uuid)
  177         return ARQ.convert_with_links(extarq.arq)
  178 
  179     @authorize_wsgi.authorize_wsgi("cyborg:arq", "get_all", False)
  180     @expose.expose(ARQCollection, wtypes.text, types.uuid)
  181     def get_all(self, bind_state=None, instance=None):
  182         """Retrieve a list of arqs."""
  183         # TODO(Sundar) Need to implement 'arq=uuid1,...' query parameter
  184         LOG.info('[arqs] get_all. bind_state:(%s), instance:(%s)',
  185                  bind_state or '', instance or '')
  186         context = pecan.request.context
  187         extarqs = objects.ExtARQ.list(context)
  188         state_map = constants.ARQ_BIND_STATES_STATUS_MAP
  189         valid_bind_states = list(state_map.keys())
  190         arqs = [extarq.arq for extarq in extarqs]
  191         # TODO(Sundar): Optimize by doing the filtering in the db layer
  192         # Apply instance filter before state filter.
  193         if bind_state and bind_state != 'resolved':
  194             raise exception.ARQBadState(
  195                 state=bind_state, uuid=None, expected=['resolved'])
  196         if instance:
  197             new_arqs = [arq for arq in arqs
  198                         if arq['instance_uuid'] == instance]
  199             arqs = new_arqs
  200             if bind_state:
  201                 for arq in new_arqs:
  202                     if arq['state'] not in valid_bind_states:
  203                         # NOTE(Sundar) This should return HTTP code 423
  204                         # if any ARQ for this instance is not resolved.
  205                         LOG.warning('Some of ARQs for instance %s is not '
  206                                     'resolved', instance)
  207                         return wsme.api.Response(
  208                             None,
  209                             status_code=HTTPStatus.LOCKED)
  210         elif bind_state:
  211             arqs = [arq for arq in arqs
  212                     if arq['state'] in valid_bind_states]
  213 
  214         ret = ARQCollection.convert_with_links(arqs)
  215         LOG.info('[arqs:get_all] Returned: %s', ret)
  216         return ret
  217 
  218     @authorize_wsgi.authorize_wsgi("cyborg:arq", "delete", False)
  219     @expose.expose(None, wtypes.text, wtypes.text,
  220                    status_code=HTTPStatus.NO_CONTENT)
  221     def delete(self, arqs=None, instance=None):
  222         """Delete one or more ARQS.
  223 
  224         The request can be either one of these two forms:
  225             DELETE /v2/accelerator_requests?arqs=uuid1,uuid2,...
  226             DELETE /v2/accelerator_requests?instance=uuid
  227 
  228         The second form is idempotent, i.e., it would have the same effect
  229         if called repeatedly with the same instance UUID. In other words,
  230         it would not raise an error on the second and later attempts even if
  231         the first one has deleted the ARQs. Whereas the first form is not
  232         idempotent: if one or more of the ARQs do not exist, it would raise
  233         an error. Nova uses the second form: so repeated calls do not cause
  234         issues.
  235 
  236         :param arqs: List of ARQ UUIDs
  237         :param instance: UUID of instance whose ARQs need to be deleted
  238         """
  239         context = pecan.request.context
  240         if (arqs and instance) or (not arqs and not instance):
  241             raise exception.ObjectActionError(
  242                 action='delete',
  243                 reason='Provide either an ARQ uuid list or an instance UUID')
  244         elif arqs:
  245             LOG.info("[arqs] delete. arqs=(%s)", arqs)
  246             pecan.request.conductor_api.arq_delete_by_uuid(context, arqs)
  247         else:  # instance is not None
  248             LOG.info("[arqs] delete. instance=(%s)", instance)
  249             pecan.request.conductor_api.arq_delete_by_instance_uuid(
  250                 context, instance)
  251 
  252     def _validate_arq_patch(self, patch):
  253         """Validate a single patch for an ARQ.
  254 
  255         :param patch: a JSON PATCH document.
  256             The patch must be of the form [{..}], as specified in the
  257             value field of arq_uuid in patch() method below.
  258         :returns: dict of valid fields
  259         """
  260         valid_fields = {'hostname': None,
  261                         'device_rp_uuid': None,
  262                         'instance_uuid': None}
  263         if utils.allow_project_id():
  264             valid_fields['project_id'] = None
  265         if ((not all(p['op'] == 'add' for p in patch)) and
  266            (not all(p['op'] == 'remove' for p in patch))):
  267             raise exception.PatchError(
  268                 reason='Every op must be add or remove')
  269 
  270         for p in patch:
  271             path = p['path'].lstrip('/')
  272             if path == 'project_id' and not utils.allow_project_id():
  273                 raise exception.NotAcceptable(_(
  274                     "Request not acceptable. The minimal required API "
  275                     "version should be %(base)s.%(opr)s") %
  276                     {'base': versions.BASE_VERSION,
  277                      'opr': versions.MINOR_1_PROJECT_ID})
  278             if path not in valid_fields.keys():
  279                 reason = 'Invalid path in patch {}'.format(p['path'])
  280                 raise exception.PatchError(reason=reason)
  281             if p['op'] == 'add':
  282                 valid_fields[path] = p['value']
  283         not_found = [field for field, value in valid_fields.items()
  284                      if value is None]
  285         if patch[0]['op'] == 'add' and len(not_found) > 0:
  286             msg = ','.join(not_found)
  287             reason = _('Fields absent in patch {}').format(msg)
  288             raise exception.PatchError(reason=reason)
  289 
  290         return valid_fields
  291 
  292     @staticmethod
  293     def _check_if_already_bound(context, valid_fields):
  294         patch_fields = list(valid_fields.values())[0]
  295         instance_uuid = patch_fields['instance_uuid']
  296         extarqs = objects.ExtARQ.list(context)
  297         extarqs_for_instance = [
  298             extarq for extarq in extarqs
  299             if extarq.arq['instance_uuid'] == instance_uuid]
  300         if extarqs_for_instance:  # duplicate binding request
  301             msg = _('Instance {} already has accelerator requests. '
  302                     'Cannot bind additional ARQs.')
  303             reason = msg.format(instance_uuid)
  304             raise exception.PatchError(reason=reason)
  305 
  306     @authorize_wsgi.authorize_wsgi("cyborg:arq", "update", False)
  307     @expose.expose(None, body=types.jsontype,
  308                    status_code=HTTPStatus.ACCEPTED)
  309     def patch(self, patch_list):
  310         """Bind/Unbind one or more ARQs.
  311 
  312         Usage: curl -X PATCH .../v2/accelerator_requests
  313                  -d <patch_list> -H "Content-type: application/json"
  314 
  315         :param patch_list: A map from ARQ UUIDs to their JSON patches:
  316             {"$arq_uuid": [
  317                 {"path": "/hostname", "op": ADD/RM, "value": "..."},
  318                 {"path": "/device_rp_uuid", "op": ADD/RM, "value": "..."},
  319                 {"path": "/instance_uuid", "op": ADD/RM, "value": "..."},
  320                 {"path": "/project_id", "op": ADD/RM, "value": "..."},
  321                ],
  322              "$arq_uuid": [...]
  323             }
  324             In particular, all and only these 4 fields must be present,
  325             and only 'add' or 'remove' ops are allowed.
  326         """
  327         LOG.info('[arqs] patch. list=(%s)', patch_list)
  328         context = pecan.request.context
  329         # Validate all patches before un/binding.
  330         valid_fields = {}
  331         for arq_uuid, patch in patch_list.items():
  332             valid_fields[arq_uuid] = self._validate_arq_patch(patch)
  333 
  334         # NOTE(Sundar): In the ARQ create/bind flow, new ARQs can be created
  335         # for a device profile any time. However, they should not be bound to
  336         # an instance which already has other ARQs bound to it. In the future,
  337         # we may allow that for hot adds, but not now.
  338         # See commit message of https://review.opendev.org/712231 for details.
  339         #
  340         # So, for bind requests, we first check that no ARQs are already
  341         # associated with the instance specified in the binding.
  342         patch = list(patch_list.values())[0]
  343         if patch[0]['op'] == 'add':
  344             self._check_if_already_bound(context, valid_fields)
  345 
  346         pecan.request.conductor_api.arq_apply_patch(
  347             context, patch_list, valid_fields)