config/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image.py

479 lines
18 KiB
Python

#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import os
import pecan
from pecan import expose
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from oslo_log import log
from sysinv._i18n import _
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.common import constants
from sysinv.common import device as dconstants
from sysinv.common import exception
from sysinv.common import utils as cutils
from sysinv import objects
LOG = log.getLogger(__name__)
ALLOWED_BITSTREAM_TYPES = [
dconstants.BITSTREAM_TYPE_ROOT_KEY,
dconstants.BITSTREAM_TYPE_FUNCTIONAL,
dconstants.BITSTREAM_TYPE_KEY_REVOCATION,
]
class DeviceImagePatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return []
class DeviceImage(base.APIBase):
"""API representation of a device_image.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
a device image.
"""
id = int
"Unique ID for this device_image"
uuid = types.uuid
"Unique UUID for this device_image"
bitstream_type = wtypes.text
"The bitstream type of the device image"
pci_vendor = wtypes.text
"The vendor ID of the pci device"
pci_device = wtypes.text
"The device ID of the pci device"
bitstream_id = wtypes.text
"The bitstream id of the functional device image"
key_signature = wtypes.text
"The key signature of the root-key device image"
revoke_key_id = int
"The key revocation id of the key revocation device image"
name = wtypes.text
"The name of the device image"
description = wtypes.text
"The description of the device image"
image_version = wtypes.text
"The version of the device image"
applied = bool
"Represent current status: created or applied"
applied_labels = types.MultiType({dict})
"Represent a list of key-value pair of labels"
def __init__(self, **kwargs):
self.fields = list(objects.device_image.fields.keys())
for k in self.fields:
setattr(self, k, kwargs.get(k))
# API-only attribute
self.fields.append('action')
setattr(self, 'action', kwargs.get('action', None))
# 'applied_labels' is not part of the object.device_image.fields
# (it is an API-only attribute)
self.fields.append('applied_labels')
setattr(self, 'applied_labels', kwargs.get('applied_labels', None))
@classmethod
def convert_with_links(cls, rpc_device_image, expand=True):
device_image = DeviceImage(**rpc_device_image.as_dict())
if not expand:
device_image.unset_fields_except(
['id', 'uuid', 'bitstream_type', 'pci_vendor', 'pci_device',
'bitstream_id', 'key_signature', 'revoke_key_id',
'name', 'description', 'image_version', 'applied_labels'])
# insert applied labels for this device image if they exist
device_image = _get_applied_labels(device_image)
# do not expose the id attribute
device_image.id = wtypes.Unset
return device_image
def _validate_bitstream_type(self):
if self.bitstream_type not in ALLOWED_BITSTREAM_TYPES:
raise ValueError(_("Bitstream type %s not supported") %
self.bitstream_type)
def validate_syntax(self):
"""
Validates the syntax of each field.
"""
self._validate_bitstream_type()
class DeviceImageCollection(collection.Collection):
"""API representation of a collection of device_image."""
device_images = [DeviceImage]
"A list containing device_image objects"
def __init__(self, **kwargs):
self._type = 'device_images'
@classmethod
def convert_with_links(cls, rpc_device_images, limit, url=None,
expand=False, **kwargs):
collection = DeviceImageCollection()
collection.device_images = [DeviceImage.convert_with_links(p, expand)
for p in rpc_device_images]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
def _get_applied_labels(device_image):
if not device_image:
return device_image
image_labels = pecan.request.dbapi.device_image_label_get_by_image(
device_image.id)
if image_labels:
applied_labels = {}
for image_label in image_labels:
label = pecan.request.dbapi.device_label_get(image_label.label_uuid)
applied_labels[label.label_key] = label.label_value
device_image.applied_labels = applied_labels
return device_image
LOCK_NAME = 'DeviceImageController'
class DeviceImageController(rest.RestController):
"""REST controller for device_image."""
def __init__(self, parent=None, **kwargs):
self._parent = parent
def _get_device_image_collection(
self, marker=None, limit=None, sort_key=None,
sort_dir=None, expand=False, resource_url=None):
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.device_image.get_by_uuid(
pecan.request.context,
marker)
deviceimages = pecan.request.dbapi.deviceimages_get_all(
limit=limit, marker=marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
return DeviceImageCollection.convert_with_links(
deviceimages, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
def _get_one(self, deviceimage_uuid):
rpc_deviceimage = objects.device_image.get_by_uuid(
pecan.request.context, deviceimage_uuid)
return DeviceImage.convert_with_links(rpc_deviceimage)
@wsme_pecan.wsexpose(DeviceImageCollection,
types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of device images."""
return self._get_device_image_collection(marker, limit,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(DeviceImage, wtypes.text)
def get_one(self, deviceimage_uuid):
"""Retrieve a single device image."""
return self._get_one(deviceimage_uuid)
@expose('json')
@cutils.synchronized(LOCK_NAME)
def post(self):
"""Create a new device image."""
fileitem = pecan.request.POST['file']
if not fileitem.filename:
return dict(success="", error="Error: No file uploaded")
try:
file_content = fileitem.file.read()
except Exception as e:
return dict(
success="",
error=("No bitstream file has been added, "
"invalid file: %s" % e))
field_list = ['uuid', 'bitstream_type', 'pci_vendor', 'pci_device',
'bitstream_id', 'key_signature', 'revoke_key_id',
'name', 'description', 'image_version']
data = dict((k, v) for (k, v) in pecan.request.POST.items()
if k in field_list and not (v is None))
msg = _validate_syntax(data)
if msg:
return dict(success="", error=msg)
device_image = pecan.request.dbapi.deviceimage_create(data)
device_image_dict = device_image.as_dict()
# Save the file contents in a temporary location
filename = cutils.format_image_filename(device_image)
image_file_path = os.path.join(dconstants.DEVICE_IMAGE_TMP_PATH, filename)
if not os.path.exists(dconstants.DEVICE_IMAGE_TMP_PATH):
os.makedirs(dconstants.DEVICE_IMAGE_TMP_PATH)
with os.fdopen(os.open(image_file_path,
os.O_CREAT | os.O_TRUNC | os.O_WRONLY,
constants.CONFIG_FILE_PERMISSION_DEFAULT),
'wb') as f:
f.write(file_content)
# Call rpc to move the bitstream file to the final destination
pecan.request.rpcapi.store_bitstream_file(pecan.request.context, filename)
return dict(success="", error="", device_image=device_image_dict)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, deviceimage_uuid):
"""Delete a device image."""
# TODO Only allow delete if there are no devices using the image
device_image = objects.device_image.get_by_uuid(
pecan.request.context, deviceimage_uuid)
filename = cutils.format_image_filename(device_image)
pecan.request.rpcapi.delete_bitstream_file(pecan.request.context,
filename)
pecan.request.dbapi.deviceimage_destroy(deviceimage_uuid)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(DeviceImage, types.uuid, wtypes.text, body=types.apidict)
def patch(self, uuid, action, body):
"""Apply/Remove a device image to/from host ."""
if action not in [dconstants.APPLY_ACTION, dconstants.REMOVE_ACTION]:
raise exception.OperationNotPermitted
try:
device_image = objects.device_image.get_by_uuid(
pecan.request.context, uuid)
except exception.DeviceImageNotFound:
LOG.error("Device image %s deos not exist." % uuid)
raise wsme.exc.ClientSideError(_(
"Device image {} failed: image does not exist".format(action)))
# For now, update status in fpga_device
# find device label with matching label key and value
for key, value in body.items():
device_labels = pecan.request.dbapi.device_label_get_by_label(
key, value)
if not device_labels:
raise wsme.exc.ClientSideError(_(
"Device image {} failed: label {}={} does not exist".format(
action, key, value)))
break
for device_label in device_labels:
if action == dconstants.APPLY_ACTION:
process_device_image_apply(device_label.pcidevice_id,
device_image, device_label.id)
# Create an entry of image to label mapping
pecan.request.dbapi.device_image_label_create({
'image_id': device_image.id,
'label_id': device_label.id,
})
update_device_image_state(device_label.host_id,
device_label.pcidevice_id,
device_image.id, dconstants.DEVICE_IMAGE_UPDATE_PENDING)
# Update flags in pci_device and host
modify_flags(device_label.pcidevice_id, device_label.host_id)
elif action == dconstants.REMOVE_ACTION:
try:
img_lbl = pecan.request.dbapi.device_image_label_get_by_image_label(
device_image.id, device_label.id)
if img_lbl:
pecan.request.dbapi.device_image_label_destroy(img_lbl.id)
except exception.DeviceImageLabelNotFoundByKey:
raise wsme.exc.ClientSideError(_(
"Device image {} not associated with label {}={}".format(
device_image.uuid, device_label.label_key,
device_label.label_value
)))
delete_device_image_state(device_label.pcidevice_id, device_image)
if not body:
# No host device labels specified, apply to all hosts
LOG.info("No host device labels specified")
hosts = pecan.request.dbapi.ihost_get_list()
for host in hosts:
fpga_devices = pecan.request.dbapi.fpga_device_get_by_host(host.id)
for dev in fpga_devices:
if action == dconstants.APPLY_ACTION:
process_device_image_apply(dev.pci_id, device_image)
update_device_image_state(host.id,
dev.pci_id, device_image.id,
dconstants.DEVICE_IMAGE_UPDATE_PENDING)
# Update flags in pci_device and host
modify_flags(dev.pci_id, dev.host_id)
elif action == dconstants.REMOVE_ACTION:
delete_device_image_state(dev.pci_id, device_image)
return DeviceImage.convert_with_links(device_image)
def _validate_bitstream_type(dev_img):
msg = None
if dev_img['bitstream_type'] not in ALLOWED_BITSTREAM_TYPES:
msg = _("Bitstream type %s not supported" % dev_img['bitstream_type'])
elif (dev_img['bitstream_type'] == dconstants.BITSTREAM_TYPE_FUNCTIONAL and
'bitstream_id' not in dev_img):
msg = _("bitstream_id is required for functional bitstream type")
elif (dev_img['bitstream_type'] == dconstants.BITSTREAM_TYPE_ROOT_KEY and
'key_signature' not in dev_img):
msg = _("key_signature is required for root key bitstream type")
elif (dev_img['bitstream_type'] == dconstants.BITSTREAM_TYPE_KEY_REVOCATION and
'revoke_key_id' not in dev_img):
msg = _("revoke_key_id is required for key revocation bitstream type")
return msg
def _is_hex_string(s):
try:
int(s, 16)
return True
except ValueError:
return False
def _validate_hexadecimal_fields(dev_img):
msg = None
if ('pci_vendor' in dev_img.keys() and
not _is_hex_string(dev_img['pci_vendor'])):
msg = _("pci_vendor must be hexadecimal")
elif ('pci_device' in dev_img.keys() and
not _is_hex_string(dev_img['pci_device'])):
msg = _("pci_device must be hexadecimal")
elif ('bitstream_id' in dev_img.keys() and
not _is_hex_string(dev_img['bitstream_id'])):
msg = _("bitstream_id must be hexadecimal")
elif ('key_signature' in dev_img.keys() and
not _is_hex_string(dev_img['key_signature'])):
msg = _("key_signature must be hexadecimal")
return msg
def _check_revoke_key(dev_img):
msg = None
if ('revoke_key_id' in dev_img.keys()):
if str(dev_img['revoke_key_id']).isdigit():
dev_img['revoke_key_id'] = int(dev_img['revoke_key_id'])
else:
msg = _("revoke_key_id must be an integer")
return msg
def _validate_syntax(device_image):
"""
Validates the syntax of each field.
"""
msg = _validate_hexadecimal_fields(device_image)
if not msg:
msg = _validate_bitstream_type(device_image)
if not msg:
msg = _check_revoke_key(device_image)
return msg
def update_device_image_state(host_id, pcidevice_id, image_id, status):
try:
dev_img_state = pecan.request.dbapi.device_image_state_get_by_image_device(
image_id, pcidevice_id)
pecan.request.dbapi.device_image_state_update(dev_img_state.id,
{'status': status})
except exception.DeviceImageStateNotFoundByKey:
# Create an entry of image to device mapping
state_values = {
'host_id': host_id,
'pcidevice_id': pcidevice_id,
'image_id': image_id,
'status': status,
}
pecan.request.dbapi.device_image_state_create(state_values)
def process_device_image_apply(pcidevice_id, device_image, label_id=None):
pci_device = pecan.request.dbapi.pci_device_get(pcidevice_id)
host = pecan.request.dbapi.ihost_get(pci_device.host_uuid)
# check if device image with type functional or root-key already applied
# to the device
records = pecan.request.dbapi.device_image_state_get_all(
host_id=host.id, pcidevice_id=pcidevice_id)
for r in records:
img = pecan.request.dbapi.deviceimage_get(r.image_id)
if img.bitstream_type == device_image.bitstream_type:
if img.bitstream_type == dconstants.BITSTREAM_TYPE_ROOT_KEY:
# Block applying root-key image if another one is already applied
msg = _("Root-key image {} is already applied to host {} device"
" {}".format(img.uuid, host.hostname, pci_device.pciaddr))
raise wsme.exc.ClientSideError(msg)
elif img.bitstream_type == dconstants.BITSTREAM_TYPE_FUNCTIONAL:
if r.status == dconstants.DEVICE_IMAGE_UPDATE_IN_PROGRESS:
msg = _("Applying image {} for host {} device {} not allowed "
"while device image update is in progress".format(
device_image.uuid, host.hostname, pci_device.pciaddr))
raise wsme.exc.ClientSideError(msg)
# Remove the existing device_image_state record
pecan.request.dbapi.device_image_state_destroy(r.uuid)
# Remove the existing device image label if any
if label_id:
try:
img_lbl = pecan.request.dbapi.device_image_label_get_by_image_label(
img.id, label_id)
pecan.request.dbapi.device_image_label_destroy(img_lbl.uuid)
except exception.DeviceImageLabelNotFoundByKey:
pass
def delete_device_image_state(pcidevice_id, device_image):
try:
dev_img = pecan.request.dbapi.device_image_state_get_by_image_device(
device_image.id, pcidevice_id)
pecan.request.dbapi.device_image_state_destroy(dev_img.uuid)
except exception.DeviceImageStateNotFoundByKey:
pass
def modify_flags(pcidevice_id, host_id):
# Set flag for pci_device indicating device requires image update
pecan.request.dbapi.pci_device_update(pcidevice_id,
{'needs_firmware_update': True},
host_id)
# Set flag for host indicating device image update is pending if it is
# not already in progress
host = pecan.request.dbapi.ihost_get(host_id)
if host.device_image_update != dconstants.DEVICE_IMAGE_UPDATE_IN_PROGRESS:
pecan.request.dbapi.ihost_update(host_id,
{'device_image_update': dconstants.DEVICE_IMAGE_UPDATE_PENDING})