diff --git a/api-ref/source/api-ref-sysinv-v1-config.rst b/api-ref/source/api-ref-sysinv-v1-config.rst index 55bde2c87b..c7b8643b1e 100644 --- a/api-ref/source/api-ref-sysinv-v1-config.rst +++ b/api-ref/source/api-ref-sysinv-v1-config.rst @@ -2036,7 +2036,7 @@ itemNotFound (404) :: - { + { "istors":[ { "function":"osd", @@ -5721,6 +5721,14 @@ itemNotFound (404) "links (Optional)", "plain", "xsd:list", "For convenience, resources contain links to themselves. This allows a client to easily obtain rather than construct resource URIs. The following types of link relations are associated with resources: a self link containing a versioned link to the resource, and a bookmark link containing a permanent link to a resource that is appropriate for long term storage." "created_at (Optional)", "plain", "xsd:dateTime", "The time when the object was created." "updated_at (Optional)", "plain", "xsd:dateTime", "The time when the object was last updated." + "needs_firmware_update (optional) ", "plain", "xsd:string", "Indicates whether the device requires firmware update." + "status (optional) ", "plain", "xsd:string", "The status of firmware update of the device." + "root_key (optional) ", "plain", "xsd:string", "The root key of the FPGA device." + "revoked_key_ids (optional) ", "plain", "xsd:string", "The revoked key ids of the FPGA device." + "boot_page (optional) ", "plain", "xsd:string", "The boot page of the FPGA device." + "bitstream_id (optional) ", "plain", "xsd:string", "The bitstream id of the FPGA device." + "bmc_build_version (optional) ", "plain", "xsd:string", "The BMC build version of the FPGA device." + "bmc_fw_version (optional) ", "plain", "xsd:string", "The BMC firmware version of the FPGA device." :: @@ -6109,7 +6117,47 @@ itemNotFound (404) "psvendor": "", "enabled": "False", "name": "pci_0000_00_0b_0" - } + }, + { + "links": [ + { + "href": "http://192.168.204.1:6385/v1/pci_devices/3ab614a6-3906-4c55-8114-4d78a6dde445", + "rel": "self" + }, + { + "href": "http://192.168.204.1:6385/pci_devices/3ab614a6-3906-4c55-8114-4d78a6dde445", + "rel": "bookmark" + } + ], + "enabled": true, + "updated_at": "2020-05-04T18:54:03.679744+00:00", + "needs_firmware_update": false, + "bitstream_id": null, + "uuid": "3ab614a6-3906-4c55-8114-4d78a6dde445", + "pdevice": "Device 0b30", + "boot_page": null, + "psvendor": "Intel Corporation", + "psdevice": "Device 0000", + "pclass_id": "120000", + "pvendor": "Intel Corporation", + "status": null, + "sriov_numvfs": 0, + "driver": "intel-fpga-pci", + "bmc_fw_version": null, + "root_key": null, + "host_uuid": "35436a7d-ce05-4e5f-87ac-706fe7513ece", + "bmc_build_version": null, + "name": "pci_0000_b3_00_0", + "revoked_key_ids": null, + "numa_node": 1, + "created_at": "2020-05-04T18:23:34.697710+00:00", + "pdevice_id": "0b30", + "pclass": "Processing accelerators", + "sriov_vfs_pci_address": "", + "sriov_totalvfs": 1, + "pciaddr": "0000:b3:00.0", + "pvendor_id": "8086" + }, ] } @@ -6310,6 +6358,531 @@ badMediaType (415) "pvendor_id": "8086" } +-------------- +Device images +-------------- + +************************ +List the device images +************************ + +.. rest_method:: GET /v1/device_images + +**Normal response codes** + +200 + +**Error response codes** + +computeFault (400, 500, ...), serviceUnavailable (503), badRequest (400), +unauthorized (401), forbidden (403), badMethod (405), overLimit (413), +itemNotFound (404) + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "device_images (Optional)", "plain", "xsd:list", "The list of device images." + "bitstream_type (Optional)", "plain", "xsd:string", "The bitstream type of the device image." + "pci_vendor (Optional)", "plain", "xsd:string", "The vendor ID of the pci device." + "pci_device (Optional)", "plain", "xsd:string", "The device ID of the pci device." + "bitstream_id (Optional)", "plain", "xsd:string", "The bitstream id of the functional device image." + "key_signature (Optional)", "plain", "xsd:string", "The key signature of the root-key device image." + "revoked_key_id (Optional)", "plain", "xsd:string", "The key revocation id of the key revocation device image." + "name (Optional)", "plain", "xsd:string", "The name of the device image." + "description (Optional)", "plain", "xsd:string", "The description of the device image." + "image_version (Optional)", "plain", "xsd:string", "The version of the device image." + "applied_labels (Optional)", "plain", "xsd:list", "The device image applied to the device labels." + "uuid (Optional)", "plain", "csapi:UUID", "The universally unique identifier for this object." + "links (Optional)", "plain", "xsd:list", "For convenience, resources contain links to themselves. This allows a client to easily obtain rather than construct resource URIs. The following types of link relations are associated with resources: a self link containing a versioned link to the resource, and a bookmark link containing a permanent link to a resource that is appropriate for long term storage." + +:: + + { + "device_images": [ + { + "uuid": "7e794693-2060-4e9e-b0bd-b281b059e8e4", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "functional", + "bitstream_id": "1234", + "key_signature": null, + "revoke_key_id": null, + "description": null, + "name": null, + "image_version": null, + "applied_labels": + { + "key1": "value1", + "key2": "value2" + }, + }, + { + "uuid": "09100124-5ae9-44d8-aefc-a192b8f27360", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "root-key", + "bitstream_id": null + "key_signature": "a123", + "revoke_key_id": null, + "name": "Image name", + "description": null, + "image_version": null, + "applied_labels": null, + }, + { + "uuid": "ef4c39b1-81e9-42dd-b850-06fc8833b47c", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "key-revocation", + "bitstream_id": null + "key_signature": null, + "revoke_key_id": 123, + "name": "Image name", + "description": null, + "image_version": null, + "applied_labels": null, + }, + ] + } + +This operation does not accept a request body. + +************************************************** +Shows attributes of the Device Image object +************************************************** + +.. rest_method:: GET /v1/device_images/​{image_id}​ + +**Normal response codes** + +200 + +**Error response codes** + +computeFault (400, 500, ...), serviceUnavailable (503), badRequest (400), +unauthorized (401), forbidden (403), badMethod (405), overLimit (413), +itemNotFound (404) + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "image_id", "URI", "csapi:UUID", "The unique identifier of a device image." + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "device_images (Optional)", "plain", "xsd:list", "The list of device images." + "bitstream_type (Optional)", "plain", "xsd:string", "The bitstream type of the device image." + "pci_vendor (Optional)", "plain", "xsd:string", "The vendor ID of the pci device ." + "pci_device (Optional)", "plain", "xsd:string", "The device ID of the pci device." + "bitstream_id (Optional)", "plain", "xsd:string", "The bitstream id of the functional device image." + "key_signature (Optional)", "plain", "xsd:string", "The key id of the root-key device image." + "revoked_key_id (Optional)", "plain", "xsd:string", "The key revocation id of the key revocation device image." + "name (Optional)", "plain", "xsd:string", "The name of the device image." + "description (Optional)", "plain", "xsd:string", "The description of the device image." + "image_version (Optional)", "plain", "xsd:string", "The version of the device image." + "applied_labels (Optional)", "plain", "xsd:list", "The device image applied to the device labels." + "uuid (Optional)", "plain", "csapi:UUID", "The universally unique identifier for this object." + "links (Optional)", "plain", "xsd:list", "For convenience, resources contain links to themselves. This allows a client to easily obtain rather than construct resource URIs. The following types of link relations are associated with resources: a self link containing a versioned link to the resource, and a bookmark link containing a permanent link to a resource that is appropriate for long term storage." + +:: + + { + "device_images": [ + { + "uuid": "7e794693-2060-4e9e-b0bd-b281b059e8e4", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "functional", + "bitstream_id": "1234", + "key_signature": null, + "revoke_key_id": null, + "description": null, + "name": null, + "image_version": null, + "applied_labels": + { + "key1": "value1", + "key2": "value2" + }, + } + ] + } + +************************ +Creates a device image +************************ + +.. rest_method:: POST /v1/device_image + +**Normal response codes** + +200 + +**Error response codes** + +badMediaType (415) + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "bitstream_type ", "plain", "xsd:string", "The bitstream type of the device image. Valid types are ``functional``, ``root-key``, ``key-revocation``" + "pci_vendor ", "plain", "xsd:string", "The vendor ID of the pci device." + "pci_device ", "plain", "xsd:string", "The device ID of the pci device." + "bitstream_id (Optional)", "plain", "xsd:string", "The bitstream id of the functional device image. Required for bitstream type ``functional`` " + "key_signature (Optional)", "plain", "xsd:string", "The key id of the root-key device image." + "revoked_key_id (Optional)", "plain", "xsd:string", "The key revocation id of the key revocation device image." + "name (Optional)", "plain", "xsd:string", "The name of the device image." + "description (Optional)", "plain", "xsd:string", "The description of the device image." + "image_version (Optional)", "plain", "xsd:string", "The version of the device image." + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "bitstream_type ", "plain", "xsd:string", "The bitstream type of the device image." + "pci_vendor ", "plain", "xsd:string", "The vendor ID of the pci device ." + "pci_device ", "plain", "xsd:string", "The device ID of the pci device." + "bitstream_id (Optional)", "plain", "xsd:string", "The bitstream id of the functional device image." + "key_signature (Optional)", "plain", "xsd:string", "The key id of the root-key device image." + "revoked_key_id (Optional)", "plain", "xsd:string", "The key revocation id of the key revocation device image." + "name (Optional)", "plain", "xsd:string", "The name of the device image." + "description (Optional)", "plain", "xsd:string", "The description of the device image." + "image_version (Optional)", "plain", "xsd:string", "The version of the device image." + "applied_labels (Optional)", "plain", "xsd:list", "The device image applied to the device labels." + "uuid (Optional)", "plain", "csapi:UUID", "The universally unique identifier for this object." + +:: + + { + "device_images": [ + { + "uuid": "7e794693-2060-4e9e-b0bd-b281b059e8e4", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "functional", + "bitstream_id": "1234", + "key_signature": null, + "revoke_key_id": null, + "description": null, + "name": null, + "image_version": null, + "applied_labels": null + } + ] + } + +************************************************ +Applies the device image to all hosts or label +************************************************ + +.. rest_method:: PATCH /v1/device_images/​{image_id}​?action=apply + +**Normal response codes** + +200 + +**Error response codes** + +badMediaType (415) + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "image_id", "URI", "csapi:UUID", "The unique identifier of a device image." + "device_label (Optional)", "plain", "xsd:string", "The key-value paired device label assigned to a device." + +:: + + { + "key1": "value1" + } + + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "bitstream_type ", "plain", "xsd:string", "The bitstream type of the device image." + "pci_vendor ", "plain", "xsd:string", "The vendor ID of the pci device ." + "pci_device ", "plain", "xsd:string", "The device ID of the pci device." + "bitstream_id (Optional)", "plain", "xsd:string", "The bitstream id of the functional device image." + "key_signature (Optional)", "plain", "xsd:string", "The key id of the root-key device image." + "revoked_key_id (Optional)", "plain", "xsd:string", "The key revocation id of the key revocation device image." + "name (Optional)", "plain", "xsd:string", "The name of the device image." + "description (Optional)", "plain", "xsd:string", "The description of the device image." + "image_version (Optional)", "plain", "xsd:string", "The version of the device image." + "applied_labels (Optional)", "plain", "xsd:list", "The device image applied to the device labels." + "uuid (Optional)", "plain", "csapi:UUID", "The universally unique identifier for this object." + +:: + + { + "device_images": [ + { + "uuid": "7e794693-2060-4e9e-b0bd-b281b059e8e4", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "functional", + "bitstream_id": "1234", + "key_signature": null, + "revoke_key_id": null, + "description": null, + "name": null, + "image_version": null, + "applied_labels": + { + "key1": "value1" + }, + } + ] + } + + +******************************************* +Remove the device image from host or label +******************************************* + +.. rest_method:: PATCH /v1/device_images/​{image_id}​?action=remove + +**Normal response codes** + +200 + +**Error response codes** + +badMediaType (415) + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "image_id", "URI", "csapi:UUID", "The unique identifier of a device image." + "device_label (Optional)", "plain", "xsd:string", "The key-value paired device label assigned to a device." + +:: + + { + "key1": "value1" + } + + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "bitstream_type ", "plain", "xsd:string", "The bitstream type of the device image." + "pci_vendor ", "plain", "xsd:string", "The vendor ID of the pci device ." + "pci_device ", "plain", "xsd:string", "The device ID of the pci device." + "bitstream_id (Optional)", "plain", "xsd:string", "The bitstream id of the functional device image." + "key_signature (Optional)", "plain", "xsd:string", "The key id of the root-key device image." + "revoked_key_id (Optional)", "plain", "xsd:string", "The key revocation id of the key revocation device image." + "name (Optional)", "plain", "xsd:string", "The name of the device image." + "description (Optional)", "plain", "xsd:string", "The description of the device image." + "image_version (Optional)", "plain", "xsd:string", "The version of the device image." + "applied_labels (Optional)", "plain", "xsd:list", "The device image applied to the device labels." + "uuid (Optional)", "plain", "csapi:UUID", "The universally unique identifier for this object." + +:: + + { + "device_images": [ + { + "uuid": "7e794693-2060-4e9e-b0bd-b281b059e8e4", + "pci_vendor": "8086", + "pci_device": "0b30", + "bitstream_type": "functional", + "bitstream_id": "1234", + "key_signature": null, + "revoke_key_id": null, + "description": null, + "name": null, + "image_version": null, + "applied_labels": null + } + ] + } + +***************************** +Deletes a device image +***************************** + +.. rest_method:: DELETE /v1/device_images/​{image_id}​ + +**Normal response codes** + +204 + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "image_id", "URI", "csapi:UUID", "The unique identifier of a device image." + +This operation does not accept a request body. + +-------------- +Device labels +-------------- + +************************ +List the device labels +************************ + +.. rest_method:: GET /v1/device_labels + +**Normal response codes** + +200 + +**Error response codes** + +computeFault (400, 500, ...), serviceUnavailable (503), badRequest (400), +unauthorized (401), forbidden (403), badMethod (405), overLimit (413), +itemNotFound (404) + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "device_labels ", "plain", "xsd:list", "The list of device labels." + "uuid (Optional)", "plain", "csapi:UUID", "The universally unique identifier for this object." + "pcidevice_uuid ", "plain", "csapi:UUID", "The universally unique identifier for the pci device object." + "host_uuid ", "plain", "csapi:UUID", "The universally unique identifier for the host object." + "label_key ", "plain", "xsd:string", "The key of the device label." + "label_value ", "plain", "xsd:string", "The value of the device label." + +:: + + { + "device_labels": [ + { + "uuid": "fe26ca98-35d4-43b7-8c51-f0ca957b35e1", + "pcidevice_uuid": "64641c6d-4fdd-4ecb-9c66-a68982267b6d", + "host_uuid": "32be8077-1174-46cf-8309-48c107765ffc" + "label_key": "key1", + "label_value": "value1", + }, + { + "uuid": "60342a18-a686-48c4-8e71-13a005ffda1b", + "pcidevice_uuid": "9d69d492-9888-4d85-90d0-e52def926b17", + "host_uuid": "32be8077-1174-46cf-8309-48c107765ffc" + "label_key": "key5", + "label_value": "value5", + }, + ] + } + +************************************* +Assign device label to a pci device +************************************* + +.. rest_method:: POST /v1/device_labels + +**Normal response codes** + +200 + +**Error response codes** + +computeFault (400, 500, ...), serviceUnavailable (503), badRequest (400), +unauthorized (401), forbidden (403), badMethod (405), overLimit (413), +itemNotFound (404) + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "pcidevice_uuid", "URI", "csapi:UUID", "The unique identifier of a pci device." + "device_labels", "URI", "xsd:list", "List of key-value paired of device labels." + +:: + + { + "pcidevice_uuid": "da98f600-49cf-4f0e-b14e-15ef91069fe8", + "key1": "value1", + "key2": "value2" + } + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "uuid", "URI", "csapi:UUID", "The unique identifier of the device label object." + "pcidevice_uuid", "URI", "csapi:UUID", "The unique identifier of a pci device." + "label_key", "URI", "xsd:string", "The label key of device labels." + "label_value", "URI", "xsd:string", "The label value of device labels." + +:: + + { + "device_labels": [ + { + "uuid": "66daffb1-72ee-4e6e-9489-206c5eeaec94", + "pcidevice_uuid": "da98f600-49cf-4f0e-b14e-15ef91069fe8", + "label_key": "key1", + "label_value": "value1", + }, + { + "uuid": "2e7821ed-e373-4cb8-a47b-f70ff2558dfd", + "pcidevice_uuid": "da98f600-49cf-4f0e-b14e-15ef91069fe8", + "label_key": "key2", + "label_value": "value2", + } + ] + } + +************************ +Deletes a device label +************************ + +.. rest_method:: DELETE /v1/device_labels/​{device_label_uuid}​ + +**Normal response codes** + +204 + +**Request parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "device_label_uuid", "URI", "csapi:UUID", "The unique identifier of a device label." + +This operation does not accept a request body. + ------------------ Service Parameter ------------------ diff --git a/sysinv/cgts-client/centos/build_srpm.data b/sysinv/cgts-client/centos/build_srpm.data index e39f04a0c8..28ff1aca4e 100644 --- a/sysinv/cgts-client/centos/build_srpm.data +++ b/sysinv/cgts-client/centos/build_srpm.data @@ -1,2 +1,2 @@ SRC_DIR="cgts-client" -TIS_PATCH_VER=75 +TIS_PATCH_VER=76 diff --git a/sysinv/cgts-client/centos/cgts-client.spec b/sysinv/cgts-client/centos/cgts-client.spec index 4e108d4b3d..0bba9d8a60 100644 --- a/sysinv/cgts-client/centos/cgts-client.spec +++ b/sysinv/cgts-client/centos/cgts-client.spec @@ -21,6 +21,7 @@ Requires: python-keystoneclient Requires: python2-oslo-i18n Requires: python2-oslo-serialization Requires: python2-oslo-utils +Requires: requests-toolbelt # Needed for python2 and python3 compatible Requires: python-six diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/common/base.py b/sysinv/cgts-client/cgts-client/cgtsclient/common/base.py index bdd9e8c55f..1d47eb9962 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/common/base.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/common/base.py @@ -57,6 +57,11 @@ class Manager(object): 'POST', url, body=body, data=data) return resp + def _upload_multipart(self, url, body, data=None): + resp = self.api.upload_request_with_multipart( + 'POST', url, body=body, data=data) + return resp + def _json_get(self, url, body=None): """send a GET request and return a json serialized object""" _, body = self.api.json_request('GET', url, body=body) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/common/http.py b/sysinv/cgts-client/cgts-client/cgtsclient/common/http.py index 9235df91b1..2db4466a45 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/common/http.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/common/http.py @@ -15,13 +15,13 @@ # under the License. # +import httplib2 import logging import os import requests +from requests_toolbelt import MultipartEncoder import socket -import httplib2 - import six from six.moves.urllib.parse import urlparse @@ -293,6 +293,19 @@ class HTTPClient(httplib2.Http): data=data) return req.json() + def upload_request_with_multipart(self, method, url, **kwargs): + self.authenticate_and_fetch_endpoint_url() + connection_url = self._get_connection_url(url) + fields = kwargs.get('data') + fields['file'] = (kwargs['body'], open(kwargs['body'], 'rb')) + enc = MultipartEncoder(fields) + headers = {'Content-Type': enc.content_type, + "X-Auth-Token": self.auth_token} + req = requests.post(connection_url, + data=enc, + headers=headers) + return req.json() + ################# # AUTHENTICATE ################# diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py index 7a7784d84d..af8bc528fb 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # @@ -26,6 +26,9 @@ from cgtsclient.v1 import certificate from cgtsclient.v1 import cluster from cgtsclient.v1 import controller_fs from cgtsclient.v1 import datanetwork +from cgtsclient.v1 import device_image +from cgtsclient.v1 import device_image_state +from cgtsclient.v1 import device_label from cgtsclient.v1 import drbdconfig from cgtsclient.v1 import ethernetport from cgtsclient.v1 import fernet @@ -165,3 +168,6 @@ class Client(http.HTTPClient): self.kube_version = kube_version.KubeVersionManager(self) self.kube_upgrade = kube_upgrade.KubeUpgradeManager(self) self.kube_host_upgrade = kube_host_upgrade.KubeHostUpgradeManager(self) + self.device_image = device_image.DeviceImageManager(self) + self.device_image_state = device_image_state.DeviceImageStateManager(self) + self.device_label = device_label.DeviceLabelManager(self) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image.py new file mode 100644 index 0000000000..a3fa2d9e78 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cgtsclient.common import base +from cgtsclient.common import utils +from cgtsclient import exc + + +CREATION_ATTRIBUTES = [ + 'bitstream_type', 'pci_vendor', 'pci_device', + 'bitstream_id', 'key_signature', 'revoke_key_id', + 'name', 'description', 'image_version', 'uuid'] + + +class DeviceImage(base.Resource): + def __repr__(self): + return "" % self._info + + +class DeviceImageManager(base.Manager): + resource_class = DeviceImage + + @staticmethod + def _path(uuid=None): + return '/v1/device_images/%s' % uuid if uuid else '/v1/device_images' + + def list(self): + return self._list(self._path(), "device_images") + + def get(self, device_image_id): + try: + return self._list(self._path(device_image_id))[0] + except IndexError: + return None + + def create(self, file, **kwargs): + data = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + data[key] = value + else: + raise exc.InvalidAttribute('%s' % key) + return self._upload_multipart(self._path(), file, data=data) + + def apply(self, device_image_id, labels=None): + return self._update(self._path(device_image_id) + '?action=apply', + labels) + + def remove(self, device_image_id, labels=None): + return self._update(self._path(device_image_id) + '?action=remove', + labels) + + def delete(self, device_image_id): + return self._delete(self._path(device_image_id)) + + +def _find_device_image(cc, device_image): + if device_image.isdigit() and not utils.is_uuid_like(device_image): + device_image_list = cc.device_image.list() + for n in device_image_list: + if str(n.id) == device_image: + return n + else: + raise exc.CommandError('device image not found: %s' % device_image) + elif utils.is_uuid_like(device_image): + try: + h = cc.device_image.get(device_image) + except exc.HTTPNotFound: + raise exc.CommandError('device image not found: %s' % device_image) + else: + return h + else: + device_image_list = cc.device_image.list() + for n in device_image_list: + if n.name == device_image: + return n + else: + raise exc.CommandError('device image not found: %s' % device_image) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_shell.py new file mode 100644 index 0000000000..1cbe03cac5 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_shell.py @@ -0,0 +1,157 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +from cgtsclient.common import utils +from cgtsclient import exc +import os + + +def _print_device_image_show(obj): + fields = ['uuid', 'bitstream_type', + 'pci_vendor', 'pci_device', + 'bitstream_id', 'key_signature', 'revoke_key_id', + 'name', 'description', 'image_version', 'applied_labels'] + + if type(obj) is dict: + data = [(f, obj.get(f, '')) for f in fields] + else: + data = [(f, getattr(obj, f, '')) for f in fields] + utils.print_tuple_list(data) + + +@utils.arg('device_image_id', + metavar='', + help="UUID or name of device_image") +def do_device_image_show(cc, args): + """Show device image details.""" + + device_image = cc.device_image.get(args.device_image_id) + _print_device_image_show(device_image) + + +def do_device_image_list(cc, args): + """List device images.""" + + labels = ['uuid', 'bitstream_type', 'pci_vendor', 'pci_device', + 'bitstream_id', 'key_signature', 'revoke_key_id', + 'name', 'description', 'image_version', 'applied_labels'] + fields = ['uuid', 'bitstream_type', 'pci_vendor', 'pci_device', + 'bitstream_id', 'key_signature', 'revoke_key_id', + 'name', 'description', 'image_version', 'applied_labels'] + device_images = cc.device_image.list() + utils.print_list(device_images, fields, labels, sortby=1) + + +@utils.arg('bitstream_file', + metavar='', + help='Path to Bitstream file [REQUIRED] ') +@utils.arg('bitstream_type', + metavar='', + choices=['root-key', 'functional', 'key-revocation'], + help="Type of the device image bitstream [REQUIRED]") +@utils.arg('pci_vendor', + metavar='', + help="PCI vendor (hexadecimal) of the device image [REQUIRED]") +@utils.arg('pci_device', + metavar='', + help="PCI device (hexadecimal) of the device image [REQUIRED]") +@utils.arg('--bitstream-id', + metavar='', + help='Bitstream ID (hexadecimal) of the functional device image') +@utils.arg('--key-signature', + metavar='', + help='Key signature (hexadecimal) of the root-key device image') +@utils.arg('--revoke-key-id', + metavar='', + help='Key ID of the key revocation device image') +@utils.arg('--name', + metavar='', + help='Name of the device image') +@utils.arg('--description', + metavar='', + help='Description of the device image') +@utils.arg('--image-version', + metavar='', + help='Version of the device image') +@utils.arg('-u', '--uuid', + metavar='', + help='UUID of the device image') +def do_device_image_create(cc, args): + """Create a device image.""" + + if not os.path.isfile(args.bitstream_file): + raise exc.CommandError('Bitstream file does not exist: %s' % + args.bitstream_file) + + field_list = ['uuid', 'bitstream_type', 'pci_vendor', 'pci_device', + 'bitstream_id', 'key_signature', 'revoke_key_id', + 'name', 'description', 'image_version'] + + # Prune input fields down to required/expected values + user_fields = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + + try: + response = cc.device_image.create(args.bitstream_file, **user_fields) + error = response.get('error') + if error: + raise exc.CommandError("%s" % error) + except exc.HTTPNotFound: + raise exc.CommandError( + 'Device image not created for %s. No response.' % args.bitstream_file) + except Exception as e: + raise exc.CommandError('Device image not created for %s: %s' % + (args.bitstream_file, e)) + else: + device_image = response.get('device_image') + _print_device_image_show(device_image) + + +@utils.arg('device_image_uuid', metavar='', + help='UUID of the device image') +@utils.arg('attributes', + metavar='', + nargs='*', + action='append', + default=[], + help="List of device labels") +def do_device_image_apply(cc, args): + """Apply the device image""" + attributes = utils.extract_keypairs(args) + try: + response = cc.device_image.apply(args.device_image_uuid, + attributes) + _print_device_image_show(response) + except exc.HTTPNotFound: + raise exc.CommandError('Device image apply failed') + + +@utils.arg('device_image_uuid', metavar='', + help='UUID of the device image') +@utils.arg('attributes', + metavar='', + nargs='*', + action='append', + default=[], + help="List of device labels") +def do_device_image_remove(cc, args): + """Remove the device image""" + attributes = utils.extract_keypairs(args) + try: + response = cc.device_image.remove(args.device_image_uuid, + attributes) + _print_device_image_show(response) + except exc.HTTPNotFound: + raise exc.CommandError('Device image remove failed') + + +@utils.arg('device_image_uuid', + metavar='', + help="UUID of device image entry") +def do_device_image_delete(cc, args): + """Delete a device image.""" + + cc.device_image.delete(args.device_image_uuid) + print('Deleted device image: %s' % args.device_image_uuid) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_state.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_state.py new file mode 100644 index 0000000000..a826f427a2 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_state.py @@ -0,0 +1,23 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cgtsclient.common import base + + +class DeviceImageState(base.Resource): + def __repr__(self): + return "" % self._info + + +class DeviceImageStateManager(base.Manager): + resource_class = DeviceImageState + + @staticmethod + def _path(uuid=None): + return '/v1/device_image_state/%s' % uuid if uuid else '/v1/device_image_state' + + def list(self): + return self._list(self._path(), "device_image_state") diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_state_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_state_shell.py new file mode 100644 index 0000000000..5e255c8da8 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_image_state_shell.py @@ -0,0 +1,24 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cgtsclient.common import utils +from cgtsclient.v1 import ihost as ihost_utils + + +def do_device_image_state_list(cc, args): + """List image to device mapping with status.""" + + device_image_state = cc.device_image_state.list() + for d in device_image_state[:]: + pdevice = cc.pci_device.get(d.pcidevice_uuid) + setattr(d, 'pciaddr', getattr(pdevice, 'pciaddr')) + host = ihost_utils._find_ihost(cc, getattr(pdevice, 'host_uuid')) + setattr(d, 'hostname', host.hostname) + labels = ['hostname', 'PCI device address', 'Device image uuid', 'status', + 'Update start time', 'updated_at'] + fields = ['hostname', 'pciaddr', 'image_uuid', 'status', + 'update_start_time', 'updated_at'] + utils.print_list(device_image_state, fields, labels, sortby=1) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_label.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_label.py new file mode 100644 index 0000000000..f8838b39aa --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_label.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cgtsclient.common import base +from cgtsclient.v1 import options + + +class DeviceLabel(base.Resource): + def __repr__(self): + return "" % self._info + + +class DeviceLabelManager(base.Manager): + resource_class = DeviceLabel + + @staticmethod + def _path(label_id=None): + return '/v1/device_labels/%s' % label_id if label_id else \ + '/v1/device_labels' + + def list(self): + path = '/v1/device_labels' + return self._list(path, "device_labels") + + def get(self, uuid): + path = '/v1/device_labels/%s' % uuid + try: + return self._list(path)[0] + except IndexError: + return None + + def assign(self, label, parameters=None): + return self._create(options.build_url(self._path(), q=None, + params=parameters), label) + + def remove(self, uuid): + return self._delete(self._path(uuid)) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_label_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_label_shell.py new file mode 100644 index 0000000000..ef2949f9cf --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/device_label_shell.py @@ -0,0 +1,120 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cgtsclient.common import utils +from cgtsclient import exc +from cgtsclient.v1 import ihost as ihost_utils +from cgtsclient.v1 import pci_device + + +def _print_device_label_show(obj): + fields = ['uuid', 'label_key', 'label_value'] + data = [(f, getattr(obj, f, '')) for f in fields] + utils.print_tuple_list(data) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host") +@utils.arg('nameorpciaddr', + metavar='', + help="Name or PCI address of device") +def do_host_device_label_list(cc, args): + """List device labels""" + host = ihost_utils._find_ihost(cc, args.hostnameorid) + device = pci_device.find_device(cc, host, args.nameorpciaddr) + device_labels = cc.device_label.list() + for dl in device_labels[:]: + if dl.pcidevice_uuid != device.uuid: + device_labels.remove(dl) + else: + setattr(dl, 'hostname', host.hostname) + setattr(dl, 'devicename', device.name) + field_labels = ['hostname', 'PCI device name', 'label key', 'label value'] + fields = ['hostname', 'devicename', 'label_key', 'label_value'] + utils.print_list(device_labels, fields, field_labels, sortby=1) + + +def do_device_label_list(cc, args): + """List all device labels""" + device_labels = cc.device_label.list() + for dl in device_labels[:]: + pci_device = cc.pci_device.get(dl.pcidevice_uuid) + setattr(dl, 'devicename', getattr(pci_device, 'name')) + host = ihost_utils._find_ihost(cc, getattr(pci_device, 'host_uuid')) + setattr(dl, 'hostname', host.hostname) + field_labels = ['hostname', 'PCI device name', 'label key', 'label value'] + fields = ['hostname', 'devicename', 'label_key', 'label_value'] + utils.print_list(device_labels, fields, field_labels, sortby=1) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host") +@utils.arg('nameorpciaddr', + metavar='', + help="Name or PCI address of device") +@utils.arg('attributes', + metavar='', + nargs='+', + action='append', + default=[], + help="List of device labels") +@utils.arg('--overwrite', + action='store_true', + help="Allow existing label values to be overwritten") +def do_host_device_label_assign(cc, args): + """Assign a label to a device of a host""" + attributes = utils.extract_keypairs(args) + parameters = ["overwrite=" + str(args.overwrite)] + host = ihost_utils._find_ihost(cc, args.hostnameorid) + device = pci_device.find_device(cc, host, args.nameorpciaddr) + attributes.update({'pcidevice_uuid': device.uuid}) + new_device_labels = cc.device_label.assign(attributes, parameters) + for p in new_device_labels.device_labels: + uuid = p['uuid'] + if uuid is not None: + try: + device_label = cc.device_label.get(uuid) + except exc.HTTPNotFound: + raise exc.CommandError('Host device label not found: %s' % uuid) + _print_device_label_show(device_label) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host [REQUIRED]") +@utils.arg('nameorpciaddr', + metavar='', + help="Name or PCI address of device") +@utils.arg('attributes', + metavar='', + nargs='+', + action='append', + default=[], + help="List of device label keys") +def do_host_device_label_remove(cc, args): + """Remove a device label from a device of a host""" + host = ihost_utils._find_ihost(cc, args.hostnameorid) + device = pci_device.find_device(cc, host, args.nameorpciaddr) + for i in args.attributes[0]: + lbl = _find_host_device_label(cc, host, device, i) + if lbl: + cc.device_label.remove(lbl.uuid) + print('Deleted device label %s for host %s device %s' % + (i, host.hostname, device.name)) + + +def _find_host_device_label(cc, host, device, label): + device_labels = cc.device_label.list() + for lbl in device_labels: + if (lbl.pcidevice_uuid == device.uuid and lbl.label_key == label): + break + else: + lbl = None + print('Host device label not found: host %s, device %s, label key %s ' % + (host.hostname, device.name, label)) + return lbl diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py index 6421885907..7833be94a5 100755 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -39,7 +39,8 @@ def _print_ihost_show(ihost, columns=None, output_format=None): 'boot_device', 'rootfs_device', 'install_output', 'console', 'tboot', 'vim_progress_status', 'software_load', 'install_state', 'install_state_info', 'inv_state', - 'clock_synchronization'] + 'clock_synchronization', + 'device_image_update', 'reboot_needed'] optional_fields = ['vsc_controllers', 'ttys_dcd'] if ihost.subfunctions != ihost.personality: fields.append('subfunctions') @@ -848,3 +849,31 @@ def do_kube_host_upgrade(cc, args): data = dict(data_list) ordereddata = OrderedDict(sorted(data.items(), key=lambda t: t[0])) utils.print_dict(ordereddata, wrap=72) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host") +def do_host_device_image_update(cc, args): + """Update device image on a host.""" + ihost = ihost_utils._find_ihost(cc, args.hostnameorid) + try: + host = cc.ihost.device_image_update(ihost.uuid) + except exc.HTTPNotFound: + raise exc.CommandError( + 'Device image update failed: host %s' % args.hostnameorid) + _print_ihost_show(host) + + +@utils.arg('hostnameorid', + metavar='', + help="Name or ID of host") +def do_host_device_image_update_abort(cc, args): + """Abort device image update on a host.""" + ihost = ihost_utils._find_ihost(cc, args.hostnameorid) + try: + host = cc.ihost.device_image_update_abort(ihost.uuid) + except exc.HTTPNotFound: + raise exc.CommandError( + 'Device image update-abort failed: host %s' % args.hostnameorid) + _print_ihost_show(host) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ihost.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ihost.py index d3f6612194..7caaf545f6 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ihost.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ihost.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -138,6 +138,16 @@ class ihostManager(base.Manager): body=post_body) return self.resource_class(self, body) + def device_image_update(self, hostid): + path = self._path(hostid) + "/device_image_update" + resp, body = self.api.json_request('POST', path) + return self.resource_class(self, body) + + def device_image_update_abort(self, hostid): + path = self._path(hostid) + "/device_image_update_abort" + resp, body = self.api.json_request('POST', path) + return self.resource_class(self, body) + def _find_ihost(cc, ihost): if ihost.isdigit() or utils.is_uuid_like(ihost): diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device.py index 15c1387382..45a6defe5e 100755 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device.py @@ -1,13 +1,11 @@ # -# Copyright (c) 2015 Wind River Systems, Inc. +# Copyright (c) 2015-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # -# -*- encoding: utf-8 -*- -# - from cgtsclient.common import base +from cgtsclient import exc class PciDevice(base.Resource): @@ -43,3 +41,13 @@ def get_pci_device_display_name(p): return p.name else: return '(' + str(p.uuid)[-8:] + ')' + + +def find_device(cc, host, nameorpciaddr): + devices = cc.pci_device.list(host.uuid) + for d in devices: + if d.name == nameorpciaddr or d.pciaddr == nameorpciaddr: + return d + else: + raise exc.CommandError('PCI device not found: host %s device %s' % + (host.hostname, nameorpciaddr)) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device_shell.py index 32ade53611..7c7bd2c4c1 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/pci_device_shell.py @@ -1,18 +1,16 @@ # -# Copyright (c) 2015 Wind River Systems, Inc. +# Copyright (c) 2015-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# All Rights Reserved. -# - from cgtsclient.common import utils from cgtsclient import exc from cgtsclient.v1 import ihost as ihost_utils +# PCI Device Class ID in hexadecimal string +PCI_DEVICE_CLASS_FPGA = '120000' + def _print_device_show(device): fields = ['name', 'pciaddr', 'pclass_id', 'pvendor_id', 'pdevice_id', @@ -26,6 +24,15 @@ def _print_device_show(device): 'sriov_vfs_pci_address', 'extra_info', 'created_at', 'updated_at'] + pclass_id = getattr(device, 'pclass_id') + if pclass_id == PCI_DEVICE_CLASS_FPGA: + fields += ['needs_firmware_update', 'status', 'root_key', + 'revoked_key_ids', 'boot_page', 'bitstream_id', + 'bmc_build_version', 'bmc_fw_version'] + labels += ['needs_firmware_update', 'status', 'root_key', + 'revoked_key_ids', 'boot_page', 'bitstream_id', + 'bmc_build_version', 'bmc_fw_version'] + data = [(f, getattr(device, f, '')) for f in fields] utils.print_tuple_list(data, labels) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py index 68f646a9f8..14d7aa7626 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -14,6 +14,9 @@ from cgtsclient.v1 import certificate_shell from cgtsclient.v1 import cluster_shell from cgtsclient.v1 import controller_fs_shell from cgtsclient.v1 import datanetwork_shell +from cgtsclient.v1 import device_image_shell +from cgtsclient.v1 import device_image_state_shell +from cgtsclient.v1 import device_label_shell from cgtsclient.v1 import drbdconfig_shell from cgtsclient.v1 import ethernetport_shell from cgtsclient.v1 import health_shell @@ -123,6 +126,9 @@ COMMAND_MODULES = [ host_fs_shell, kube_version_shell, kube_upgrade_shell, + device_image_shell, + device_image_state_shell, + device_label_shell, ] diff --git a/sysinv/cgts-client/cgts-client/requirements.txt b/sysinv/cgts-client/cgts-client/requirements.txt index be7aee85cf..b4859f301e 100644 --- a/sysinv/cgts-client/cgts-client/requirements.txt +++ b/sysinv/cgts-client/cgts-client/requirements.txt @@ -3,3 +3,4 @@ keyring oslo.i18n # Apache-2.0 oslo.serialization>=1.10.0,!=2.19.1 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 +requests-toolbelt diff --git a/sysinv/sysinv/centos/build_srpm.data b/sysinv/sysinv/centos/build_srpm.data index f491e011d2..748442f42c 100644 --- a/sysinv/sysinv/centos/build_srpm.data +++ b/sysinv/sysinv/centos/build_srpm.data @@ -1,2 +1,2 @@ SRC_DIR="sysinv" -TIS_PATCH_VER=345 +TIS_PATCH_VER=346 diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py index 1b11f3c361..ab20ce6c0a 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -28,6 +28,9 @@ from sysinv.api.controllers.v1 import cluster from sysinv.api.controllers.v1 import community from sysinv.api.controllers.v1 import controller_fs from sysinv.api.controllers.v1 import cpu +from sysinv.api.controllers.v1 import device_image +from sysinv.api.controllers.v1 import device_image_state +from sysinv.api.controllers.v1 import device_label from sysinv.api.controllers.v1 import disk from sysinv.api.controllers.v1 import datanetwork from sysinv.api.controllers.v1 import interface_datanetwork @@ -261,6 +264,15 @@ class V1(base.APIBase): kube_host_upgrades = [link.Link] "Links to the kube_host_upgrade resource" + device_images = [link.Link] + "Links to the device images resource" + + device_image_state = [link.Link] + "Links to the device image state resource" + + device_labels = [link.Link] + "Links to the device labels resource" + @classmethod def convert(self): v1 = V1() @@ -809,6 +821,26 @@ class V1(base.APIBase): 'kube_host_upgrades', '', bookmark=True)] + v1.device_images = [link.Link.make_link('self', pecan.request.host_url, + 'device_images', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'device_images', '', + bookmark=True)] + + v1.device_image_state = [link.Link.make_link('self', pecan.request.host_url, + 'device_image_state', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'device_image_state', '', + bookmark=True)] + + v1.device_labels = [link.Link.make_link('self', pecan.request.host_url, + 'device_labels', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'device_labels', '', + bookmark=True)] return v1 @@ -880,6 +912,9 @@ class Controller(rest.RestController): kube_versions = kube_version.KubeVersionController() kube_upgrade = kube_upgrade.KubeUpgradeController() kube_host_upgrades = kube_host_upgrade.KubeHostUpgradeController() + device_images = device_image.DeviceImageController() + device_image_state = device_image_state.DeviceImageStateController() + device_labels = device_label.DeviceLabelController() @wsme_pecan.wsexpose(V1) def get(self): diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image.py new file mode 100644 index 0000000000..1ac610aa34 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image.py @@ -0,0 +1,478 @@ +# +# 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}) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image_state.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image_state.py new file mode 100644 index 0000000000..87bca406ff --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_image_state.py @@ -0,0 +1,152 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pecan +from pecan import rest +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan +from oslo_log import log +from sysinv.api.controllers.v1 import base +from sysinv.api.controllers.v1 import collection +from sysinv.api.controllers.v1 import link +from sysinv.api.controllers.v1 import types +from sysinv.api.controllers.v1 import utils +from sysinv import objects + +LOG = log.getLogger(__name__) + + +class DeviceImageState(base.APIBase): + """API representation of a device_image_state. + + 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_state" + + uuid = types.uuid + "Unique UUID for this device_image_state" + + host_id = int + "Represent the host id of the host that the pci_device belongs to" + + host_uuid = types.uuid + "Represent the UUID of the host that the pci_device belongs to" + + pcidevice_id = int + "Represent the id of pci_device" + + pcidevice_uuid = types.uuid + "Represent the uuid of pci_device" + + image_id = int + "Represent the id of device image" + + image_uuid = types.uuid + "Represent the uuid of device image" + + status = wtypes.text + "Firmware update status" + + update_start_time = wtypes.datetime.datetime + "Represents the start time of the device image update" + + updated_at = wtypes.datetime.datetime + "The time at which the record is updated " + + links = [link.Link] + "A list containing a self link and associated device image state links" + + def __init__(self, **kwargs): + self.fields = list(objects.device_image_state.fields.keys()) + for k in self.fields: + setattr(self, k, kwargs.get(k)) + + @classmethod + def convert_with_links(cls, rpc_device_image_state, expand=True): + device_image_state = DeviceImageState(**rpc_device_image_state.as_dict()) + if not expand: + device_image_state.unset_fields_except( + ['id', 'uuid', 'host_id', 'host_uuid', + 'pcidevice_id', 'pcidevice_uuid', + 'image_id', 'image_uuid', 'status', + 'update_start_time', 'updated_at']) + + # do not expose the id attribute + device_image_state.host_id = wtypes.Unset + return device_image_state + + +class DeviceImageStateCollection(collection.Collection): + """API representation of a collection of device_image_state.""" + + device_image_state = [DeviceImageState] + "A list containing device_image_state objects" + + def __init__(self, **kwargs): + self._type = 'device_image_state' + + @classmethod + def convert_with_links(cls, rpc_device_image_state, limit, url=None, + expand=False, **kwargs): + collection = DeviceImageStateCollection() + collection.device_image_state = [DeviceImageState.convert_with_links(p, expand) + for p in rpc_device_image_state] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +LOCK_NAME = 'DeviceImageStateController' + + +class DeviceImageStateController(rest.RestController): + """REST controller for device image state.""" + + def __init__(self, parent=None, **kwargs): + self._parent = parent + + def _get_device_image_state_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_state.get_by_uuid( + pecan.request.context, + marker) + + states = pecan.request.dbapi.device_image_state_get_list( + limit=limit, marker=marker_obj, + sort_key=sort_key, sort_dir=sort_dir) + + return DeviceImageStateCollection.convert_with_links( + states, limit, url=resource_url, expand=expand, + sort_key=sort_key, sort_dir=sort_dir) + + def _get_one(self, uuid): + obj = objects.device_image_state.get_by_uuid( + pecan.request.context, uuid) + return DeviceImageState.convert_with_links(obj) + + @wsme_pecan.wsexpose(DeviceImageStateCollection, + 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 image state.""" + + return self._get_device_image_state_collection(marker, limit, + sort_key=sort_key, + sort_dir=sort_dir) + + @wsme_pecan.wsexpose(DeviceImageState, wtypes.text) + def get_one(self, deviceimagestate_uuid): + """Retrieve a single device image state.""" + + return self._get_one(deviceimagestate_uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_label.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_label.py new file mode 100644 index 0000000000..5edf9b3709 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/device_label.py @@ -0,0 +1,244 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pecan +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 exception +from sysinv.common import utils as cutils +from sysinv import objects + +LOG = log.getLogger(__name__) + + +class DeviceLabelPatchType(types.JsonPatchType): + @staticmethod + def mandatory_attrs(): + return [] + + +class DeviceLabel(base.APIBase): + """API representation of a device label. + + This class enforces type checking and value constraints, and converts + between the internal object model and the API representation of + a device label. + """ + + id = int + "Unique ID for this device label" + + uuid = types.uuid + "Unique UUID for this device label" + + host_id = int + "Represent the id of host the device label belongs to" + + host_uuid = types.uuid + "Represent the uuid of the host the device label belongs to" + + pcidevice_id = int + "Represent the id of pci_device the device label belongs to" + + pcidevice_uuid = types.uuid + "Represent the uuid of the pci_device the device label belongs to" + + label_key = wtypes.text + "Represents a label key assigned to the device" + + label_value = wtypes.text + "Represents a label value assigned to the device" + + def __init__(self, **kwargs): + self.fields = list(objects.device_label.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)) + + @classmethod + def convert_with_links(cls, rpc_device_label, expand=True): + device_label = DeviceLabel(**rpc_device_label.as_dict()) + if not expand: + device_label.unset_fields_except( + ['uuid', 'host_id', 'host_uuid', 'pcidevice_id', 'pcidevice_uuid', + 'label_key', 'label_value']) + + # do not expose the id attribute + device_label.host_id = wtypes.Unset + device_label.pcidevice_id = wtypes.Unset + + return device_label + + +class DeviceLabelCollection(collection.Collection): + """API representation of a collection of device label.""" + + device_labels = [DeviceLabel] + "A list containing device_label objects" + + def __init__(self, **kwargs): + self._type = 'device_labels' + + @classmethod + def convert_with_links(cls, rpc_device_labels, limit, url=None, + expand=False, **kwargs): + collection = DeviceLabelCollection() + collection.device_labels = [DeviceLabel.convert_with_links(p, expand) + for p in rpc_device_labels] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +LOCK_NAME = 'DeviceLabelController' + + +class DeviceLabelController(rest.RestController): + """REST controller for device label.""" + + def __init__(self, parent=None, **kwargs): + self._parent = parent + + def _get_device_label_collection( + self, device_uuid, marker=None, limit=None, sort_key=None, + sort_dir=None, expand=False, resource_url=None): + if self._parent and not device_uuid: + raise exception.InvalidParameterValue(_( + "Device id not specified.")) + + limit = utils.validate_limit(limit) + sort_dir = utils.validate_sort_dir(sort_dir) + marker_obj = None + if marker: + marker_obj = objects.device_label.get_by_uuid( + pecan.request.context, + marker) + + if device_uuid: + device_labels = pecan.request.dbapi.device_label_get_by_device( + device_uuid, limit, + marker_obj, + sort_key=sort_key, + sort_dir=sort_dir) + else: + device_labels = pecan.request.dbapi.device_label_get_list( + limit, marker_obj, + sort_key=sort_key, + sort_dir=sort_dir) + + return DeviceLabelCollection.convert_with_links( + device_labels, limit, url=resource_url, expand=expand, + sort_key=sort_key, sort_dir=sort_dir) + + def _get_one(self, device_label_uuid): + rpc_device_label = objects.device_label.get_by_uuid( + pecan.request.context, device_label_uuid) + return DeviceLabel.convert_with_links(rpc_device_label) + + @wsme_pecan.wsexpose(DeviceLabelCollection, types.uuid, types.uuid, + int, wtypes.text, wtypes.text) + def get_all(self, uuid=None, marker=None, limit=None, + sort_key='id', sort_dir='asc'): + """Retrieve a list of device labels.""" + return self._get_device_label_collection(uuid, marker, limit, + sort_key=sort_key, + sort_dir=sort_dir) + + @wsme_pecan.wsexpose(DeviceLabel, types.uuid) + def get_one(self, device_label_uuid): + """Retrieve a single device label.""" + + try: + sp_label = objects.device_label.get_by_uuid( + pecan.request.context, + device_label_uuid) + except exception.InvalidParameterValue: + raise wsme.exc.ClientSideError( + _("No device label found for %s" % device_label_uuid)) + + return DeviceLabel.convert_with_links(sp_label) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(DeviceLabelCollection, types.boolean, + body=types.apidict) + def post(self, overwrite=False, body=None): + """Assign a new device label.""" + + pcidevice_uuid = body['pcidevice_uuid'] + del body['pcidevice_uuid'] + pcidevice = objects.pci_device.get_by_uuid(pecan.request.context, + pcidevice_uuid) + fpgadevice = pecan.request.dbapi.fpga_device_get(pcidevice.pciaddr, + pcidevice.host_id) + + existing_labels = {} + for label_key in body.keys(): + label = None + try: + label = pecan.request.dbapi.device_label_query( + pcidevice.id, label_key) + except exception.DeviceLabelNotFoundByKey: + pass + if label: + if overwrite: + existing_labels.update({label_key: label.uuid}) + else: + raise wsme.exc.ClientSideError(_( + "Label %s exists for device %s. Use overwrite option" + " to assign a new value." % + (label_key, pcidevice.name))) + + new_records = [] + for key, value in body.items(): + values = { + 'host_id': pcidevice.host_id, + 'pcidevice_id': pcidevice.id, + 'fpgadevice_id': fpgadevice.id, + 'label_key': key, + 'label_value': value + } + try: + if existing_labels.get(key, None): + # Update the value + label_uuid = existing_labels.get(key) + new_label = pecan.request.dbapi.device_label_update( + label_uuid, {'label_value': value}) + else: + new_label = pecan.request.dbapi.device_label_create( + pcidevice_uuid, values) + new_records.append(new_label) + except exception.DeviceLabelAlreadyExists: + # We should not be here + raise wsme.exc.ClientSideError(_( + "Error creating label %s") % label_key) + + return DeviceLabelCollection.convert_with_links( + new_records, limit=None, url=None, expand=False, + sort_key='id', sort_dir='asc') + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(None, types.uuid, status_code=204) + def delete(self, device_label_uuid): + """Delete a device label.""" + + pecan.request.dbapi.device_label_destroy(device_label_uuid) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(DeviceLabel, body=DeviceLabel) + def patch(self, device_label): + """Modify a new device label.""" + raise exception.OperationNotPermitted diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py index 9af76ece4c..ee03212386 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py @@ -88,6 +88,7 @@ from sysinv.api.controllers.v1 import patch_api from sysinv.common import ceph from sysinv.common import constants +from sysinv.common import device from sysinv.common import exception from sysinv.common import kubernetes from sysinv.common import utils as cutils @@ -515,6 +516,12 @@ class Host(base.APIBase): iscsi_initiator_name = wtypes.text "The iscsi initiator name (only used for worker hosts)" + device_image_update = wtypes.text + "Represent the status of device image update of this ihost." + + reboot_needed = types.boolean + " Represent whether a reboot is needed after device image update" + def __init__(self, **kwargs): self.fields = list(objects.host.fields.keys()) for k in self.fields: @@ -1088,6 +1095,8 @@ class HostController(rest.RestController): 'wipe_osds': ['GET'], 'kube_upgrade_control_plane': ['POST'], 'kube_upgrade_kubelet': ['POST'], + 'device_image_update': ['POST'], + 'device_image_update_abort': ['POST'], } def __init__(self, from_isystem=False): @@ -6842,6 +6851,45 @@ class HostController(rest.RestController): host_obj.hostname) return Host.convert_with_links(host_obj) + # POST ihosts//device_image_update + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(Host, types.uuid) + def device_image_update(self, host_uuid): + """ Update device image on the specified host. + :param host_uuid: UUID of the host + """ + LOG.info("device_image_update host_uuid=%s " % host_uuid) + host_obj = objects.host.get_by_uuid(pecan.request.context, host_uuid) + + # Set the flag indicating the host is in progress of + # updating device image + host_obj = pecan.request.dbapi.ihost_update(host_uuid, + {'device_image_update': device.DEVICE_IMAGE_UPDATE_IN_PROGRESS}) + # Call rpcapi to tell conductor to begin device image update + pecan.request.rpcapi.host_device_image_update( + pecan.request.context, host_uuid) + return Host.convert_with_links(host_obj) + + # POST ihosts//device_image_update_abort + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(Host, types.uuid) + def device_image_update_abort(self, host_uuid): + """ Abort device image update on the specified host. + :param host_uuid: UUID of the host + :param install_uuid: install_uuid. + """ + LOG.info("device_image_update_abort host_uuid=%s " % host_uuid) + host_obj = objects.host.get_by_uuid(pecan.request.context, host_uuid) + + # Set the flag indicating the host is no longer updating the device + # image + pecan.request.dbapi.ihost_update(host_uuid, + {'device_image_update': device.DEVICE_IMAGE_UPDATE_PENDING}) + # Call rpcapi to tell conductor to abort device image update + pecan.request.rpcapi.host_device_image_update_abort( + pecan.request.context, host_uuid) + return Host.convert_with_links(host_obj) + def _create_node(host, xml_node, personality, is_dynamic_ip): host_node = et.SubElement(xml_node, 'host') diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/pci_device.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/pci_device.py index fcf3c1f035..f52501d341 100755 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/pci_device.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/pci_device.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015-2016 Wind River Systems, Inc. +# Copyright (c) 2015-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -18,6 +18,7 @@ from sysinv.api.controllers.v1 import link 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 @@ -103,6 +104,30 @@ class PCIDevice(base.APIBase): enabled = types.boolean "Represent the enabled status of the device" + bmc_build_version = wtypes.text + "Represent the BMC build version of the fpga device" + + bmc_fw_version = wtypes.text + "Represent the BMC firmware version of the fpga device" + + root_key = wtypes.text + "Represent the root key of the fpga device" + + revoked_key_ids = wtypes.text + "Represent the key revocation ids of the fpga device" + + boot_page = wtypes.text + "Represent the boot page of the fpga device" + + bitstream_id = wtypes.text + "Represent the bitstream id of the fpga device" + + needs_firmware_update = types.boolean + "Represent whether firmware update is required for the fpga device" + + status = wtypes.text + "Represent the status of the fpga device" + links = [link.Link] "Represent a list containing a self link and associated device links" @@ -123,12 +148,25 @@ class PCIDevice(base.APIBase): 'sriov_totalvfs', 'sriov_numvfs', 'sriov_vfs_pci_address', 'driver', 'host_uuid', 'enabled', + 'bmc_build_version', 'bmc_fw_version', + 'root_key', 'revoked_key_ids', + 'boot_page', 'bitstream_id', + 'needs_firmware_update', 'status', 'created_at', 'updated_at']) # do not expose the id attribute device.host_id = wtypes.Unset device.node_id = wtypes.Unset + # if not FPGA device, hide these attributes + if device.pclass_id != dconstants.PCI_DEVICE_CLASS_FPGA: + device.bmc_build_version = wtypes.Unset + device.bmc_fw_version = wtypes.Unset + device.root_key = wtypes.Unset + device.revoked_key_ids = wtypes.Unset + device.boot_page = wtypes.Unset + device.bitstream_id = wtypes.Unset + device.links = [link.Link.make_link('self', pecan.request.host_url, 'pci_devices', device.uuid), link.Link.make_link('bookmark', @@ -241,6 +279,7 @@ class PCIDeviceController(rest.RestController): rpc_device = objects.pci_device.get_by_uuid( pecan.request.context, device_uuid) + return PCIDevice.convert_with_links(rpc_device) @cutils.synchronized(LOCK_NAME) diff --git a/sysinv/sysinv/sysinv/sysinv/api/hooks.py b/sysinv/sysinv/sysinv/sysinv/api/hooks.py index cdc5b3857a..c529985b1c 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/hooks.py +++ b/sysinv/sysinv/sysinv/sysinv/api/hooks.py @@ -215,6 +215,8 @@ class AuditLogging(hooks.PecanHook): url_path = urlparse(state.request.path_qs).path def json_post_data(rest_state): + if 'form-data' in rest_state.request.headers.get('Content-Type'): + return " POST: {}".format(rest_state.request.params) if not hasattr(rest_state.request, 'json'): return "" return " POST: {}".format(rest_state.request.json) diff --git a/sysinv/sysinv/sysinv/sysinv/common/constants.py b/sysinv/sysinv/sysinv/sysinv/common/constants.py index f04eba8167..74ec2b0b7a 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/common/constants.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # diff --git a/sysinv/sysinv/sysinv/sysinv/common/device.py b/sysinv/sysinv/sysinv/sysinv/common/device.py new file mode 100644 index 0000000000..55e3edd704 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/common/device.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# PCI Device Class ID in hexidecimal string +PCI_DEVICE_CLASS_FPGA = '120000' + +# Device Image +DEVICE_IMAGE_TMP_PATH = '/tmp/device_images' +DEVICE_IMAGE_PATH = '/opt/platform/device_images' + +BITSTREAM_TYPE_ROOT_KEY = 'root-key' +BITSTREAM_TYPE_FUNCTIONAL = 'functional' +BITSTREAM_TYPE_KEY_REVOCATION = 'key-revocation' + +# Device Image Status +DEVICE_IMAGE_UPDATE_PENDING = 'pending' +DEVICE_IMAGE_UPDATE_IN_PROGRESS = 'in-progress' +DEVICE_IMAGE_UPDATE_COMPLETED = 'completed' +DEVICE_IMAGE_UPDATE_FAILED = 'failed' + +# Device Image Action +APPLY_ACTION = 'apply' +REMOVE_ACTION = 'remove' diff --git a/sysinv/sysinv/sysinv/sysinv/common/exception.py b/sysinv/sysinv/sysinv/sysinv/common/exception.py index 43f159faa4..0402edda85 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/exception.py +++ b/sysinv/sysinv/sysinv/sysinv/common/exception.py @@ -387,6 +387,10 @@ class PCIAddrAlreadyExists(Conflict): "for %(host)s already exists.") +class PCIAddrNotFound(Conflict): + message = _("A Device with PCI address %(pciaddr)s could not be found.") + + class LvmLvgAlreadyExists(Conflict): message = _("LVM Local Volume Group %(name)s for %(host)s already exists.") @@ -1318,6 +1322,83 @@ class FilesystemAlreadyExists(Conflict): class FilesystemNotFound(NotFound): message = _("Host FS with id %(fs_id)s not found") + +# Device image +class UnsupportedDeviceImageBitstreamType(Conflict): + message = _("Device image with bitstream type '%(bitstream_type)s' " + "is not supported.") + + +class DeviceImageNotFound(NotFound): + message = _("Device image %(deviceimage_uuid)s could not be found.") + + +class DeviceImageTypeNotFound(NotFound): + message = _("Device image of type %(bitstream_type)s could not be found.") + + +class DeviceImageIDNotFound(NotFound): + message = _("Device image with id %(id)s could not be found.") + + +class DeviceImageNameNotFound(NotFound): + message = _("Device image with name %(name)s could not be found.") + + +class DeviceImageAlreadyExists(Conflict): + message = _("Device image of name %(name)s already exists.") + + +class DeviceImageTypeUnsupported(Conflict): + message = _("Device image of type %(bitstream_type)s is not supported.") + + +# Device Label +class DeviceLabelNotFound(NotFound): + message = _("Device label %(uuid)s could not be found.") + + +class DeviceLabelAlreadyExists(Conflict): + message = _("Device label %(label)s already " + "exists on this host %(host)s.") + + +class DeviceLabelNotFoundByKey(NotFound): + message = _("Device label %(label)s could not be found.") + + +class DeviceLabelInvalid(Invalid): + message = _("Device label is invalid. Reason: %(reason)s") + + +# Device Image Label +class DeviceImageLabelNotFound(NotFound): + message = _("Device image label %(uuid)s could not be found.") + + +class DeviceImageLabelAlreadyExists(Conflict): + message = _("Device image is already applied to label %(uuid)s.") + + +class DeviceImageLabelNotFoundByKey(NotFound): + message = _("Device image %(image_id)s " + "and label ID %(label_id)s not found") + + +# Device Image State +class DeviceImageStateAlreadyExists(Conflict): + message = _( + "A device to image mapping with id %(uuid)s already exists.") + + +class DeviceImageStateNotFound(NotFound): + message = _("A device to image mapping with id %(id)s not found") + + +class DeviceImageStateNotFoundByKey(NotFound): + message = _("Device image %(image_id)s " + "and device ID %(device_id)s not found") + # # Kubernetes application and Helm related exceptions # diff --git a/sysinv/sysinv/sysinv/sysinv/common/utils.py b/sysinv/sysinv/sysinv/sysinv/common/utils.py index 48eacc2702..12539b3823 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/common/utils.py @@ -2216,3 +2216,11 @@ def extract_certs_from_pem(pem_contents): certs.append(cert) start = start + index + len(marker) return certs + + +def format_image_filename(device_image): + """ Format device image filename """ + return "{}-{}-{}-{}.bit".format(device_image.bitstream_type, + device_image.pci_vendor, + device_image.pci_device, + device_image.uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index 8575f2e9f4..84bf4c296f 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -81,6 +81,7 @@ from sysinv.api.controllers.v1 import utils from sysinv.api.controllers.v1 import vim_api from sysinv.common import constants from sysinv.common import ceph as cceph +from sysinv.common import device as dconstants from sysinv.common import exception from sysinv.common import image_versions from sysinv.common import fm @@ -11376,3 +11377,37 @@ class ConductorManager(service.PeriodicService): kube_upgrade_obj = objects.kube_upgrade.get_one(context) kube_upgrade_obj.state = kubernetes.KUBE_UPGRADED_NETWORKING kube_upgrade_obj.save() + + def store_bitstream_file(self, context, filename): + """Store FPGA bitstream file """ + image_file_path = os.path.join(dconstants.DEVICE_IMAGE_PATH, filename) + image_tmp_path = os.path.join(dconstants.DEVICE_IMAGE_TMP_PATH, filename) + try: + os.makedirs(dconstants.DEVICE_IMAGE_PATH) + except OSError as oe: + if (oe.errno != errno.EEXIST or + not os.path.isdir(dconstants.DEVICE_IMAGE_PATH)): + LOG.error("Failed to create dir %s" % dconstants.DEVICE_IMAGE_PATH) + raise + shutil.copyfile(image_tmp_path, image_file_path) + LOG.info("copied %s to %s" % (image_tmp_path, image_file_path)) + + def delete_bitstream_file(self, context, filename): + """Delete FPGA bitstream file""" + image_file_path = os.path.join(dconstants.DEVICE_IMAGE_PATH, filename) + try: + os.remove(image_file_path) + except OSError: + LOG.exception("Failed to delete bitstream file %s" % image_file_path) + + def host_device_image_update(self, context, host_uuid): + """Update the device image on this host""" + + host_obj = objects.host.get_by_uuid(context, host_uuid) + LOG.info("Updating device image on %s" % host_obj.hostname) + + def host_device_image_update_abort(self, context, host_uuid): + """Abort device image update on this host""" + + host_obj = objects.host.get_by_uuid(context, host_uuid) + LOG.info("Aborting device image update on %s" % host_obj.hostname) diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py index 9da49c3741..86edca0b40 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py @@ -16,7 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # """ @@ -1891,3 +1891,43 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy): """ return self.cast(context, self.make_msg('kube_upgrade_networking', kube_version=kube_version)) + + def store_bitstream_file(self, context, filename): + """Asynchronously, have the conductor store the device image + on this host. + + :param context: request context + :param filename: name of the bitstream file + """ + return self.cast(context, self.make_msg('store_bitstream_file', + filename=filename)) + + def delete_bitstream_file(self, context, filename): + """Asynchronously, have the conductor remove the device image + on this host. + + :param context: request context + :param filename: name of the bitstream file + """ + return self.cast(context, self.make_msg('delete_bitstream_file', + filename=filename)) + + def host_device_image_update(self, context, host_uuid): + """Asynchronously, have the conductor update the device image + on this host. + + :param context: request context + :param host_uuid: uuid or id of the host + """ + return self.cast(context, self.make_msg('host_device_image_update', + host_uuid=host_uuid)) + + def host_device_image_update_abort(self, context, host_uuid): + """Asynchronously, have the conductor abort the device image update + on this host. + + :param context: request context + :param host_uuid: uuid or id of the host + """ + return self.cast(context, self.make_msg('host_device_image_update_abort', + host_uuid=host_uuid)) diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py index c8dfce0c40..c2c7aa9b37 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py @@ -15,7 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # """SQLAlchemy storage backend.""" @@ -45,6 +45,7 @@ from oslo_utils import uuidutils from sysinv._i18n import _ from sysinv import objects from sysinv.common import constants +from sysinv.common import device as dconstants from sysinv.common import exception from sysinv.common import utils from sysinv.db import api @@ -1149,6 +1150,26 @@ def add_host_fs_filter_by_ihost(query, value): return query.filter(models.ihost.uuid == value) +def add_deviceimage_filter(query, value): + """Adds a deviceimage-specific filter to a query. + + :param query: Initial query to add filter to. + :param value: Value for filtering results by. + :return: Modified query. + """ + + if uuidutils.is_uuid_like(value): + return query.filter(or_(models.DeviceImageRootKey.uuid == value, + models.DeviceImageFunctional.uuid == value, + models.DeviceImageKeyRevocation.uuid == value)) + elif utils.is_int_like(value): + return query.filter(or_(models.DeviceImageRootKey.id == value, + models.DeviceImageFunctional.id == value, + models.DeviceImageKeyRevocation.id == value)) + else: + return add_identity_filter(query, value, use_name=True) + + class Connection(api.Connection): """SqlAlchemy connection.""" @@ -1793,6 +1814,87 @@ class Connection(api.Connection): filter_by(id=memory_id).\ delete() + @objects.objectify(objects.fpga_device) + def fpga_device_create(self, hostid, values): + + if utils.is_int_like(hostid): + host = self.ihost_get(int(hostid)) + elif utils.is_uuid_like(hostid): + host = self.ihost_get(hostid.strip()) + elif isinstance(hostid, models.ihost): + host = hostid + else: + raise exception.NodeNotFound(node=hostid) + + values['host_id'] = host['id'] + + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + fpga_device = models.FpgaDevice() + fpga_device.update(values) + with _session_for_write() as session: + try: + session.add(fpga_device) + session.flush() + except db_exc.DBDuplicateEntry: + LOG.error("Failed to add FPGA device (uuid: %s), FPGA device with PCI " + "address %s on host %s already exists" % + (values['uuid'], + values['pciaddr'], + values['host_id'])) + raise exception.PCIAddrAlreadyExists(pciaddr=values['pciaddr'], + host=values['host_id']) + return self._fpga_device_get(values['pciaddr'], values['host_id']) + + def _fpga_device_get(self, pciaddr, hostid=None): + query = model_query(models.FpgaDevice) + if hostid: + query = query.filter_by(host_id=hostid) + query = add_identity_filter(query, pciaddr, use_pciaddr=True) + try: + result = query.one() + except NoResultFound: + raise exception.PCIAddrNotFound(pciaddr=pciaddr) + + return result + + @objects.objectify(objects.fpga_device) + def fpga_device_get(self, deviceid, hostid=None): + return self._fpga_device_get(deviceid, hostid) + + @objects.objectify(objects.fpga_device) + def fpga_device_update(self, device_id, values, forihostid=None): + with _session_for_write() as session: + # May need to reserve in multi controller system; ref sysinv + query = model_query(models.FpgaDevice, read_deleted="no", + session=session) + + if forihostid: + query = query.filter_by(host_id=forihostid) + + try: + query = add_identity_filter(query, device_id) + result = query.one() + for k, v in values.items(): + setattr(result, k, v) + except NoResultFound: + raise exception.InvalidParameterValue( + err="No entry found for device %s" % device_id) + except MultipleResultsFound: + raise exception.InvalidParameterValue( + err="Multiple entries found for device %s" % device_id) + + return query.one() + + @objects.objectify(objects.fpga_device) + def fpga_device_get_by_host(self, host, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.FpgaDevice) + query = add_device_filter_by_host(query, host) + return _paginate_query(models.FpgaDevice, limit, marker, + sort_key, sort_dir, query) + @objects.objectify(objects.pci_device) def pci_device_create(self, hostid, values): @@ -8259,3 +8361,423 @@ class Connection(api.Connection): except NoResultFound: raise exception.KubeUpgradeNotFound(upgrade_id=upgrade_id) query.delete() + + def _deviceimage_get(self, model_class, deviceimage_id, obj=None): + session = None + if obj: + session = inspect(obj).session + query = model_query(model_class, session=session) + + query = add_deviceimage_filter(query, deviceimage_id) + + try: + result = query.one() + except NoResultFound: + raise exception.DeviceImageNotFound( + deviceimage_uuid=deviceimage_id) + except MultipleResultsFound: + raise exception.InvalidParameterValue( + err="Multiple entries found for deviceimage %s" % deviceimage_id) + return result + + def _deviceimage_get_one(self, deviceimage_id, deviceimage=None): + entity = with_polymorphic(models.DeviceImage, '*') + query = model_query(entity) + query = add_deviceimage_filter(query, deviceimage_id) + if deviceimage is not None: + query = query.filter_by(network_type=deviceimage) + try: + result = query.one() + except NoResultFound: + raise exception.DeviceImageNotFound( + deviceimage_uuid=deviceimage_id) + except MultipleResultsFound: + raise exception.InvalidParameterValue( + err="Multiple entries found for deviceimage %s" % deviceimage_id) + + return result + + def _deviceimage_create(self, obj, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + with _session_for_write() as session: + # The id is null for ae interfaces with more than one member interface + temp_id = obj.id + obj.update(values) + if obj.id is None: + obj.id = temp_id + + try: + session.add(obj) + session.flush() + except db_exc.DBDuplicateEntry: + LOG.error("Failed to add deviceimage (uuid: %s), " + "name %s already exists." % + (values['uuid'], values.get('name'))) + + raise exception.DeviceImageAlreadyExists( + name=values.get('name')) + + return self._deviceimage_get(type(obj), values['uuid']) + + @objects.objectify(objects.device_image) + def deviceimage_create(self, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + bitstream_type = values.get('bitstream_type') + if bitstream_type == dconstants.BITSTREAM_TYPE_ROOT_KEY: + deviceimage = models.DeviceImageRootKey() + elif bitstream_type == dconstants.BITSTREAM_TYPE_FUNCTIONAL: + deviceimage = models.DeviceImageFunctional() + elif bitstream_type == dconstants.BITSTREAM_TYPE_KEY_REVOCATION: + deviceimage = models.DeviceImageKeyRevocation() + else: + raise exception.DeviceImageTypeUnsupported( + bitstream_type=bitstream_type) + return self._deviceimage_create(deviceimage, values) + + @objects.objectify(objects.device_image) + def deviceimage_get(self, deviceimage_id): + return self._deviceimage_get_one(deviceimage_id) + + def _add_deviceimage_filters(self, query, filters): + if filters is None: + filters = dict() + supported_filters = {'bitstream_type', + 'name', + } + unsupported_filters = set(filters).difference(supported_filters) + if unsupported_filters: + msg = _("SqlAlchemy API does not support " + "filtering by %s") % ', '.join(unsupported_filters) + raise ValueError(msg) + + for field in supported_filters: + if field in filters: + query = query.filter_by(**{field: filters[field]}) + + return query + + @objects.objectify(objects.device_image) + def deviceimages_get_all(self, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + + with _session_for_read() as session: + deviceimages = with_polymorphic(models.DeviceImage, '*') + query = model_query(deviceimages, session=session) + query = self._add_deviceimage_filters(query, filters) + + return _paginate_query(models.DeviceImage, limit, marker, + sort_key, sort_dir, query) + + @objects.objectify(objects.device_image) + def deviceimage_update(self, deviceimage_uuid, values): + with _session_for_write() as session: + query = model_query(models.DeviceImage, session=session) + query = add_identity_filter(query, deviceimage_uuid) + + count = query.update(values, synchronize_session='fetch') + if count != 1: + raise exception.DeviceImageNotFound( + deviceimage_uuid=deviceimage_uuid) + return query.one() + + def deviceimage_destroy(self, deviceimage_uuid): + query = model_query(models.DeviceImage) + query = add_identity_filter(query, deviceimage_uuid) + try: + query.one() + except NoResultFound: + raise exception.DeviceImageNotFound( + deviceimage_uuid=deviceimage_uuid) + query.delete() + + def _device_label_get(self, device_label_id): + query = model_query(models.DeviceLabel) + query = add_identity_filter(query, device_label_id) + + try: + result = query.one() + except NoResultFound: + raise exception.DeviceLabelNotFound(uuid=device_label_id) + return result + + @objects.objectify(objects.device_label) + def device_label_create(self, device_uuid, values): + + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + values['device_uuid'] = device_uuid + + host_device_label = models.DeviceLabel() + host_device_label.update(values) + with _session_for_write() as session: + try: + session.add(host_device_label) + session.flush() + except db_exc.DBDuplicateEntry: + LOG.error("Failed to add host device label %s. " + "Already exists with this uuid" % + (values['label_key'])) + raise exception.DeviceLabelAlreadyExists( + label=values['label_key'], host=values['host_uuid']) + return self._device_label_get(values['uuid']) + + @objects.objectify(objects.device_label) + def device_label_get(self, uuid): + query = model_query(models.DeviceLabel) + query = query.filter_by(uuid=uuid) + try: + result = query.one() + except NoResultFound: + raise exception.InvalidParameterValue( + err="No device label entry found for %s" % uuid) + return result + + @objects.objectify(objects.device_label) + def device_label_get_all(self, deviceid=None): + query = model_query(models.DeviceLabel, read_deleted="no") + if deviceid: + query = query.filter_by(device_id=deviceid) + return query.all() + + @objects.objectify(objects.device_label) + def device_label_get_list(self, limit=None, marker=None, + sort_key=None, sort_dir=None): + return _paginate_query(models.DeviceLabel, limit, marker, + sort_key, sort_dir) + + @objects.objectify(objects.device_label) + def device_label_get_by_label(self, label_key, label_value, + limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.DeviceLabel) + query = query.filter_by(label_key=label_key, + label_value=label_value) + return query.all() + + @objects.objectify(objects.device_label) + def device_label_update(self, uuid, values): + with _session_for_write() as session: + query = model_query(models.DeviceLabel, session=session) + query = query.filter_by(uuid=uuid) + + count = query.update(values, synchronize_session='fetch') + if count == 0: + raise exception.DeviceLabelNotFound(uuid) + return query.one() + + def device_label_destroy(self, uuid): + with _session_for_write() as session: + query = model_query(models.DeviceLabel, session=session) + query = query.filter_by(uuid=uuid) + try: + query.one() + except NoResultFound: + raise exception.DeviceLabelNotFound(uuid) + query.delete() + + @objects.objectify(objects.device_label) + def device_label_get_by_device(self, device_uuid, + limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.DeviceLabel) + query = query.filter_by(pcidevice_uuid=device_uuid) + return _paginate_query(models.DeviceLabel, limit, marker, + sort_key, sort_dir, query) + + def _device_label_query(self, device_id, label_key, session=None): + query = model_query(models.DeviceLabel, session=session) + query = query.filter(models.DeviceLabel.pcidevice_id == device_id) + query = query.filter(models.DeviceLabel.label_key == label_key) + try: + result = query.one() + except NoResultFound: + raise exception.DeviceLabelNotFoundByKey(label=label_key) + return result + + @objects.objectify(objects.device_label) + def device_label_query(self, device_id, label_key): + return self._device_label_query(device_id, label_key) + + def count_hosts_by_device_label(self, device_label): + query = model_query(models.DeviceLabel, read_deleted="no") + query = query.filter(models.DeviceLabel.label_key == device_label) + return query.count() + + def _device_image_label_get(self, device_image_label_id): + query = model_query(models.DeviceImageLabel) + query = add_identity_filter(query, device_image_label_id) + + try: + result = query.one() + except NoResultFound: + raise exception.DeviceLabelNotFound(uuid=device_image_label_id) + return result + + @objects.objectify(objects.device_image_label) + def device_image_label_create(self, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + device_image_label = models.DeviceImageLabel() + device_image_label.update(values) + with _session_for_write() as session: + try: + session.add(device_image_label) + session.flush() + except db_exc.DBDuplicateEntry: + raise exception.DeviceImageLabelAlreadyExists( + uuid=values['uuid']) + return self._device_image_label_get(values['uuid']) + + @objects.objectify(objects.device_image_label) + def device_image_label_get(self, uuid): + query = model_query(models.DeviceImageLabel) + query = query.filter_by(uuid=uuid) + try: + result = query.one() + except NoResultFound: + raise exception.InvalidParameterValue( + err="No device image label entry found for %s" % uuid) + return result + + @objects.objectify(objects.device_image_label) + def device_image_label_update(self, uuid, values): + with _session_for_write() as session: + query = model_query(models.DeviceImageLabel, session=session) + query = query.filter_by(uuid=uuid) + + count = query.update(values, synchronize_session='fetch') + if count == 0: + raise exception.DeviceImageLabelNotFound(uuid) + return query.one() + + @objects.objectify(objects.device_image_label) + def device_image_label_get_by_image(self, image_id, + limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.DeviceImageLabel) + query = query.filter_by(image_id=image_id) + return query.all() + + @objects.objectify(objects.device_image_label) + def device_image_label_get_by_image_label(self, image_id, label_id, + limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.DeviceImageLabel) + query = query.filter_by(image_id=image_id, label_id=label_id) + try: + return query.one() + except NoResultFound: + raise exception.DeviceImageLabelNotFoundByKey( + image_id=image_id, label_id=label_id) + + def device_image_label_destroy(self, id): + with _session_for_write() as session: + query = model_query(models.DeviceImageLabel, session=session) + query = add_identity_filter(query, id) + + try: + query.one() + except NoResultFound: + raise exception.DeviceImageLabelNotFound(uuid=id) + query.delete() + + def _device_image_state_get(self, id): + query = model_query(models.DeviceImageState) + query = add_identity_filter(query, id) + + try: + return query.one() + except NoResultFound: + raise exception.DeviceImageStateNotFound(id=id) + + @objects.objectify(objects.device_image_state) + def device_image_state_create(self, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + device_image_state = models.DeviceImageState() + device_image_state.update(values) + with _session_for_write() as session: + try: + session.add(device_image_state) + session.flush() + except db_exc.DBDuplicateEntry: + raise exception.DeviceImageStateAlreadyExists(uuid=values['uuid']) + return self._device_image_state_get(values['uuid']) + + @objects.objectify(objects.device_image_state) + def device_image_state_get(self, id): + return self._device_image_state_get(id) + + @objects.objectify(objects.device_image_state) + def device_image_state_get_one(self): + query = model_query(models.DeviceImageState) + + try: + return query.one() + except NoResultFound: + raise exception.NotFound() + + @objects.objectify(objects.device_image_state) + def device_image_state_get_list(self, limit=None, marker=None, + sort_key=None, sort_dir=None): + + query = model_query(models.DeviceImageState) + + return _paginate_query(models.DeviceImageState, limit, marker, + sort_key, sort_dir, query) + + @objects.objectify(objects.device_image_state) + def device_image_state_update(self, id, values): + with _session_for_write() as session: + query = model_query(models.DeviceImageState, session=session) + query = add_identity_filter(query, id) + + count = query.update(values, synchronize_session='fetch') + if count != 1: + raise exception.DeviceImageStateNotFound(id=id) + return query.one() + + def device_image_state_destroy(self, id): + with _session_for_write() as session: + query = model_query(models.DeviceImageState, session=session) + query = add_identity_filter(query, id) + + try: + query.one() + except NoResultFound: + raise exception.DeviceImageStateNotFound(id=id) + query.delete() + + @objects.objectify(objects.device_image_state) + def device_image_state_get_by_image_device(self, image_id, pcidevice_id, + limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.DeviceImageState) + query = query.filter_by(image_id=image_id, + pcidevice_id=pcidevice_id) + try: + return query.one() + except NoResultFound: + raise exception.DeviceImageStateNotFoundByKey(image_id=image_id, + device_id=pcidevice_id) + + @objects.objectify(objects.device_image_state) + def device_image_state_get_all(self, host_id=None, pcidevice_id=None, + image_id=None, status=None, + limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.DeviceImageState) + if host_id: + query = query.filter_by(host_id=host_id) + if pcidevice_id: + query = query.filter_by(pcidevice_id=pcidevice_id) + if image_id: + query = query.filter_by(image_id=image_id) + if status: + query = query.filter_by(status=status) + return query.all() diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/099_placeholder.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/099_placeholder.py new file mode 100644 index 0000000000..b99c1d0cd5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/099_placeholder.py @@ -0,0 +1,14 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/100_placeholder.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/100_placeholder.py new file mode 100644 index 0000000000..b99c1d0cd5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/100_placeholder.py @@ -0,0 +1,14 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/101_placeholder.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/101_placeholder.py new file mode 100644 index 0000000000..b99c1d0cd5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/101_placeholder.py @@ -0,0 +1,14 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/102_placeholder.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/102_placeholder.py new file mode 100644 index 0000000000..b99c1d0cd5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/102_placeholder.py @@ -0,0 +1,14 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/103_placeholder.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/103_placeholder.py new file mode 100644 index 0000000000..b99c1d0cd5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/103_placeholder.py @@ -0,0 +1,14 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/104_fpga_devices.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/104_fpga_devices.py new file mode 100644 index 0000000000..541bd83d3c --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/104_fpga_devices.py @@ -0,0 +1,78 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sqlalchemy import Column, MetaData, Table +from sqlalchemy import String, Integer, DateTime, Boolean +from sqlalchemy import ForeignKey, UniqueConstraint + + +ENGINE = 'InnoDB' +CHARSET = 'utf8' + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + Table('i_host', meta, autoload=True) + pci_devices = Table('pci_devices', meta, autoload=True) + + fpga_devices = Table( + 'fpga_devices', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + Column('host_id', Integer, ForeignKey('i_host.id', + ondelete='CASCADE')), + Column('pci_id', Integer, ForeignKey('pci_devices.id', + ondelete='CASCADE')), + + Column('pciaddr', String(32)), + Column('bmc_build_version', String(32)), + Column('bmc_fw_version', String(32)), + Column('root_key', String(128)), + Column('revoked_key_ids', String(512)), + Column('boot_page', String(16)), + Column('bitstream_id', String(32)), + + UniqueConstraint('pciaddr', 'host_id', name='u_pciaddrhost'), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + fpga_devices.create() + + Table('ports', meta, autoload=True) + + fpga_ports = Table( + 'fpga_ports', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + Column('port_id', Integer, ForeignKey('ports.id', ondelete='CASCADE')), + Column('fpga_id', Integer, ForeignKey('fpga_devices.id', ondelete='CASCADE')), + UniqueConstraint('port_id', 'fpga_id', name='u_port_id@fpga_id'), + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + fpga_ports.create() + + # Add new fields to pci_device table + pci_devices.create_column(Column('status', String(128))) + pci_devices.create_column(Column('needs_firmware_update', Boolean, default=False)) + + +def downgrade(migrate_engine): + # Downgrade is unsupported in this release. + raise NotImplementedError('SysInv database downgrade is unsupported.') diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/105_device_images.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/105_device_images.py new file mode 100644 index 0000000000..6e0ce2b531 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/105_device_images.py @@ -0,0 +1,201 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sqlalchemy import DateTime, String, Integer, Boolean, Text +from sqlalchemy import Column, MetaData, Table +from sqlalchemy import ForeignKey, UniqueConstraint + +ENGINE = 'InnoDB' +CHARSET = 'utf8' + + +def upgrade(migrate_engine): + """ + This database upgrade creates a device_images, device_labels and + device_image_state tables. + """ + + meta = MetaData() + meta.bind = migrate_engine + + device_images = Table( + 'device_images', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + + Column('bitstream_type', String(255)), + # The pci_vendor and pci_device fields cannot be referenced from the + # pci_devices table. The device images intended for a specific + # vendor/device on a subcloud may not be present on the + # SystemController region + Column('pci_vendor', String(4)), + Column('pci_device', String(4)), + Column('name', String(255)), + Column('description', String(255)), + Column('image_version', String(255)), + Column('applied', Boolean, nullable=False, default=False), + Column('capabilities', Text), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + device_images_rootkey = Table( + 'device_images_rootkey', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, + ForeignKey('device_images.id', ondelete="CASCADE"), + primary_key=True, nullable=False), + + Column('key_signature', String(255), nullable=False), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + device_images_functional = Table( + 'device_images_functional', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, + ForeignKey('device_images.id', ondelete="CASCADE"), + primary_key=True, nullable=False), + + Column('bitstream_id', String(255), nullable=False), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + device_images_keyrevocation = Table( + 'device_images_keyrevocation', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, + ForeignKey('device_images.id', ondelete="CASCADE"), + primary_key=True, nullable=False), + + Column('revoke_key_id', Integer, nullable=False), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + host = Table('i_host', meta, autoload=True) + Table('pci_devices', meta, autoload=True) + Table('fpga_devices', meta, autoload=True) + device_labels = Table( + 'device_labels', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + + Column('host_id', Integer, + ForeignKey('i_host.id', ondelete='CASCADE')), + Column('pcidevice_id', Integer, + ForeignKey('pci_devices.id', ondelete='CASCADE')), + Column('fpgadevice_id', Integer, + ForeignKey('fpga_devices.id', ondelete='CASCADE')), + Column('label_key', String(384)), + Column('label_value', String(128)), + Column('capabilities', Text), + + UniqueConstraint('pcidevice_id', 'label_key', + name='u_pcidevice_id@label_key'), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + device_image_labels = Table( + 'device_image_labels', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + Column('image_id', Integer, + ForeignKey('device_images.id', ondelete='CASCADE')), + Column('label_id', Integer, + ForeignKey('device_labels.id', ondelete='CASCADE')), + Column('status', String(128)), + Column('capabilities', Text), + + UniqueConstraint('image_id', 'label_id', name='u_image_id@label_id'), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + device_image_state = Table( + 'device_image_state', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, primary_key=True, nullable=False), + Column('uuid', String(36), unique=True), + Column('host_id', Integer, + ForeignKey('i_host.id', ondelete='CASCADE')), + Column('pcidevice_id', Integer, + ForeignKey('pci_devices.id', ondelete='CASCADE')), + Column('image_id', Integer, + ForeignKey('device_images.id', ondelete='CASCADE')), + Column('status', String(128)), + Column('update_start_time', DateTime), + Column('capabilities', Text), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + + tables = ( + device_images, + device_images_rootkey, + device_images_functional, + device_images_keyrevocation, + device_labels, + device_image_labels, + device_image_state, + ) + + for index, table in enumerate(tables): + try: + table.create() + except Exception: + # If an error occurs, drop all tables created so far to return + # to the previously existing state. + meta.drop_all(tables=tables[:index]) + raise + + # Add the device_image_update attribute + host.create_column(Column('device_image_update', String(64))) + host.create_column(Column('reboot_needed', Boolean, default=False)) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + # Downgrade is unsupported. + raise NotImplementedError('SysInv database downgrade is unsupported.') diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py index 5dbd636fe2..87d6a00760 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py @@ -15,7 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -234,6 +234,9 @@ class ihost(Base): ttys_dcd = Column(Boolean) iscsi_initiator_name = Column(String(64)) + device_image_update = Column(String(64)) + reboot_needed = Column(Boolean, nullable=False, default=False) + forisystemid = Column(Integer, ForeignKey('i_system.id', ondelete='CASCADE')) peer_id = Column(Integer, @@ -1459,11 +1462,170 @@ class PciDevice(Base): enabled = Column(Boolean) extra_info = Column(Text) - host = relationship("ihost", lazy="joined", join_depth=1) + status = Column(String(128)) + needs_firmware_update = Column(Boolean, nullable=False, default=False) + host = relationship("ihost", lazy="joined", join_depth=1) + fpga = relationship("FpgaDevice", lazy="joined", uselist=False, join_depth=1) UniqueConstraint('pciaddr', 'host_id', name='u_pciaddrhost') +class FpgaDevice(Base): + __tablename__ = 'fpga_devices' + + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(36)) + host_id = Column(Integer, ForeignKey('i_host.id', ondelete='CASCADE')) + pci_id = Column(Integer, ForeignKey('pci_devices.id', ondelete='CASCADE')) + pciaddr = Column(String(32)) + bmc_build_version = Column(String(32)) + bmc_fw_version = Column(String(32)) + root_key = Column(String(128)) + revoked_key_ids = Column(String(512)) + boot_page = Column(String(16)) + bitstream_id = Column(String(32)) + + host = relationship("ihost", lazy="joined", join_depth=1) + pcidevice = relationship("PciDevice", lazy="joined", join_depth=1) + UniqueConstraint('pciaddr', 'host_id', name='u_pciaddrhost') + + +class FpgaPorts(Base): + __tablename__ = 'fpga_ports' + + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(36), unique=True) + + port_id = Column(Integer, ForeignKey('ports.id', ondelete='CASCADE')) + fpga_id = Column(Integer, + ForeignKey('fpga_devices.id', ondelete='CASCADE')) + + ports = relationship("Ports", lazy="joined", join_depth=1) + fpga_device = relationship("FpgaDevice", lazy="joined", + backref="fpga_ports", join_depth=1) + UniqueConstraint('port_id', 'fpga_id', name='u_port_id@fpga_id') + + +class DeviceImage(Base): + __tablename__ = 'device_images' + + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + + bitstream_type = Column(String(255)) + pci_vendor = Column(String(4)) + pci_device = Column(String(4)) + name = Column(String(255)) + description = Column(String(255)) + image_version = Column(String(255)) + applied = Column(Boolean, nullable=False, default=False) + capabilities = Column(JSONEncodedDict) + __mapper_args__ = { + 'polymorphic_identity': 'deviceimage', + 'polymorphic_on': bitstream_type, + 'with_polymorphic': '*', + } + + +class DeviceImageCommon(object): + @declared_attr + def id(cls): + return Column(Integer, + ForeignKey('device_images.id', ondelete="CASCADE"), + primary_key=True, nullable=False) + + +class DeviceImageRootKey(DeviceImageCommon, DeviceImage): + __tablename__ = 'device_images_rootkey' + + key_signature = Column(String(255), nullable=True) + __mapper_args__ = { + 'polymorphic_identity': 'root-key', + } + + +class DeviceImageFunctional(DeviceImageCommon, DeviceImage): + __tablename__ = 'device_images_functional' + + bitstream_id = Column(String(255), nullable=True) + + __mapper_args__ = { + 'polymorphic_identity': 'functional', + } + + +class DeviceImageKeyRevocation(DeviceImageCommon, DeviceImage): + __tablename__ = 'device_images_keyrevocation' + + revoke_key_id = Column(Integer, nullable=True) + + __mapper_args__ = { + 'polymorphic_identity': 'key-revocation', + } + + +class DeviceLabel(Base): + __tablename__ = 'device_labels' + + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + host_id = Column(Integer, ForeignKey('i_host.id', ondelete='CASCADE')) + pcidevice_id = Column(Integer, ForeignKey('pci_devices.id', + ondelete='CASCADE')) + fpgadevice_id = Column(Integer, ForeignKey('fpga_devices.id', + ondelete='CASCADE')) + capabilities = Column(JSONEncodedDict) + + host = relationship("ihost", lazy="joined", join_depth=1) + pcidevice = relationship("PciDevice", lazy="joined", join_depth=1) + fpgadevice = relationship("FpgaDevice", lazy="joined", join_depth=1) + label_key = Column(String(384)) + label_value = Column(String(128)) + UniqueConstraint('pcidevice_id', 'label_key', name='u_pcidevice_id@label_key') + + +class DeviceImageLabel(Base): + __tablename__ = 'device_image_labels' + + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(36), unique=True) + + image_id = Column( + Integer, ForeignKey('device_images.id', ondelete='CASCADE')) + label_id = Column( + Integer, ForeignKey('device_labels.id', ondelete='CASCADE')) + status = Column(String(128)) + capabilities = Column(JSONEncodedDict) + + image = relationship( + "DeviceImage", lazy="joined", backref="device_image_labels") + label = relationship( + "DeviceLabel", lazy="joined", backref="device_image_labels") + UniqueConstraint('image_id', 'label_id', name='u_image_id@label_id') + + +class DeviceImageState(Base): + __tablename__ = 'device_image_state' + + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(36), unique=True) + + host_id = Column(Integer, ForeignKey('i_host.id', ondelete='CASCADE')) + pcidevice_id = Column( + Integer, ForeignKey('pci_devices.id', ondelete='CASCADE')) + image_id = Column( + Integer, ForeignKey('device_images.id', ondelete='CASCADE')) + status = Column(String(128)) + update_start_time = Column(DateTime(timezone=False)) + capabilities = Column(JSONEncodedDict) + + host = relationship("ihost", lazy="joined", join_depth=1) + pcidevice = relationship( + "PciDevice", lazy="joined", backref="device_image_state") + image = relationship( + "DeviceImage", lazy="joined", backref="device_image_state") + + class SoftwareUpgrade(Base): __tablename__ = 'software_upgrade' diff --git a/sysinv/sysinv/sysinv/sysinv/objects/__init__.py b/sysinv/sysinv/sysinv/sysinv/objects/__init__.py index c73a24bad5..ab04884b78 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/__init__.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2013-2019 Wind River Systems, Inc. +# Copyright (c) 2013-2020 Wind River Systems, Inc. # @@ -28,7 +28,12 @@ from sysinv.objects import community from sysinv.objects import controller_fs from sysinv.objects import cpu from sysinv.objects import datanetwork +from sysinv.objects import device_image +from sysinv.objects import device_image_label +from sysinv.objects import device_image_state +from sysinv.objects import device_label from sysinv.objects import disk +from sysinv.objects import fpga_device from sysinv.objects import partition from sysinv.objects import dns from sysinv.objects import drbdconfig @@ -195,6 +200,11 @@ kube_upgrade = kube_upgrade.KubeUpgrade kube_version = kube_version.KubeVersion datanetwork = datanetwork.DataNetwork host_fs = host_fs.HostFS +device_image = device_image.DeviceImage +device_image_label = device_image_label.DeviceImageLabel +device_image_state = device_image_state.DeviceImageState +device_label = device_label.DeviceLabel +fpga_device = fpga_device.FPGADevice __all__ = (system, cluster, @@ -268,6 +278,10 @@ __all__ = (system, datanetwork, interface_network, host_fs, + device_image, + device_image_label, + device_label, + fpga_device, # alias objects for RPC compatibility ihost, ilvg, diff --git a/sysinv/sysinv/sysinv/sysinv/objects/device_image.py b/sysinv/sysinv/sysinv/sysinv/objects/device_image.py new file mode 100644 index 0000000000..28b583c927 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/device_image.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class DeviceImage(base.SysinvObject): + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = {'id': int, + 'uuid': utils.uuid_or_none, + 'bitstream_type': utils.str_or_none, + 'pci_vendor': utils.str_or_none, + 'pci_device': utils.str_or_none, + 'bitstream_id': utils.str_or_none, + 'key_signature': utils.str_or_none, + 'revoke_key_id': utils.int_or_none, + 'name': utils.str_or_none, + 'description': utils.str_or_none, + 'image_version': utils.str_or_none, + 'applied': utils.bool_or_none, + 'capabilities': utils.dict_or_none, + } + + _optional_fields = {'bitstream_id', + 'key_signature', + 'revoke_key_id', + 'name', + 'description', + 'image_version'} + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.deviceimage_get(uuid) + + def save_changes(self, context, updates): + self.dbapi.device_image_update(self.uuid, # pylint: disable=no-member + updates) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/device_image_label.py b/sysinv/sysinv/sysinv/sysinv/objects/device_image_label.py new file mode 100644 index 0000000000..3d80b701ef --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/device_image_label.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class DeviceImageLabel(base.SysinvObject): + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = {'id': int, + 'uuid': utils.uuid_or_none, + 'image_id': utils.int_or_none, + 'image_uuid': utils.uuid_or_none, + 'label_id': utils.int_or_none, + 'label_uuid': utils.uuid_or_none, + 'status': utils.str_or_none, + 'capabilities': utils.dict_or_none, + } + + _foreign_fields = { + 'image_id': 'image:id', + 'label_id': 'label:id', + 'image_uuid': 'image:uuid', + 'label_uuid': 'label:uuid', + } + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.device_image_label_get(uuid) + + def save_changes(self, context, updates): + self.dbapi.device_image_label_update(self.uuid, # pylint: disable=no-member + updates) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/device_image_state.py b/sysinv/sysinv/sysinv/sysinv/objects/device_image_state.py new file mode 100644 index 0000000000..260a618811 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/device_image_state.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class DeviceImageState(base.SysinvObject): + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = {'id': int, + 'uuid': utils.uuid_or_none, + 'host_id': utils.int_or_none, + 'host_uuid': utils.uuid_or_none, + 'pcidevice_id': utils.int_or_none, + 'pcidevice_uuid': utils.uuid_or_none, + 'image_id': utils.int_or_none, + 'image_uuid': utils.uuid_or_none, + 'status': utils.str_or_none, + 'update_start_time': utils.datetime_or_str_or_none, + 'capabilities': utils.dict_or_none, + } + + _foreign_fields = { + 'host_uuid': 'host:uuid', + 'pcidevice_uuid': 'pcidevice:uuid', + 'image_uuid': 'image:uuid', + } + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.device_image_state_get(uuid) + + def save_changes(self, context, updates): + self.dbapi.device_image_state_update(self.uuid, # pylint: disable=no-member + updates) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/device_label.py b/sysinv/sysinv/sysinv/sysinv/objects/device_label.py new file mode 100644 index 0000000000..4068bdd286 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/device_label.py @@ -0,0 +1,43 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class DeviceLabel(base.SysinvObject): + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': int, + 'uuid': utils.str_or_none, + 'host_id': utils.str_or_none, + 'host_uuid': utils.str_or_none, + 'label_key': utils.str_or_none, + 'label_value': utils.str_or_none, + 'pcidevice_id': utils.int_or_none, + 'pcidevice_uuid': utils.str_or_none, + 'fpgadevice_id': utils.int_or_none, + 'fpgadevice_uuid': utils.str_or_none, + 'capabilities': utils.dict_or_none, + } + + _foreign_fields = { + 'host_uuid': 'host:uuid', + 'pcidevice_uuid': 'pcidevice:uuid', + 'fpgadevice_uuid': 'fpgadevice:uuid', + } + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.device_label_get(uuid) + + def save_changes(self, context, updates): + self.dbapi.device_label_update(self.uuid, # pylint: disable=no-member + updates) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/fpga_device.py b/sysinv/sysinv/sysinv/sysinv/objects/fpga_device.py new file mode 100644 index 0000000000..52cc7f1b4e --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/fpga_device.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class FPGADevice(base.SysinvObject): + + dbapi = db_api.get_instance() + + fields = { + 'id': int, + 'uuid': utils.str_or_none, + 'host_id': utils.int_or_none, + 'host_uuid': utils.str_or_none, + 'pci_id': utils.int_or_none, + 'pciaddr': utils.str_or_none, + 'bmc_build_version': utils.str_or_none, + 'bmc_fw_version': utils.str_or_none, + 'root_key': utils.str_or_none, + 'revoked_key_ids': utils.str_or_none, + 'boot_page': utils.str_or_none, + 'bitstream_id': utils.str_or_none, + } + + _foreign_fields = { + 'host_uuid': 'host:uuid' + } + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + return cls.dbapi.fpga_device_get(uuid) + + def save_changes(self, context, updates): + self.dbapi.fpga_device_update(self.uuid, # pylint: disable=no-member + updates) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/host.py b/sysinv/sysinv/sysinv/sysinv/objects/host.py index 8d5d546442..1195414ca8 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/host.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/host.py @@ -89,6 +89,8 @@ class Host(base.SysinvObject): 'install_state': utils.str_or_none, 'install_state_info': utils.str_or_none, 'iscsi_initiator_name': utils.str_or_none, + 'device_image_update': utils.str_or_none, + 'reboot_needed': utils.bool_or_none, } _foreign_fields = { diff --git a/sysinv/sysinv/sysinv/sysinv/objects/pci_device.py b/sysinv/sysinv/sysinv/sysinv/objects/pci_device.py index 88211837ae..24f62c94c8 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/pci_device.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/pci_device.py @@ -1,13 +1,9 @@ # -# Copyright (c) 2016 Wind River Systems, Inc. +# Copyright (c) 2020 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# coding=utf-8 -# - from sysinv.db import api as db_api from sysinv.objects import base from sysinv.objects import utils @@ -39,10 +35,34 @@ class PCIDevice(base.SysinvObject): 'driver': utils.str_or_none, 'enabled': utils.bool_or_none, 'extra_info': utils.str_or_none, + + 'bmc_build_version': utils.str_or_none, + 'bmc_fw_version': utils.str_or_none, + 'root_key': utils.str_or_none, + 'revoked_key_ids': utils.str_or_none, + 'boot_page': utils.str_or_none, + 'bitstream_id': utils.str_or_none, + 'status': utils.str_or_none, + 'needs_firmware_update': utils.bool_or_none, } _foreign_fields = { - 'host_uuid': 'host:uuid' + 'host_uuid': 'host:uuid', + 'bmc_build_version': 'fpga:bmc_build_version', + 'bmc_fw_version': 'fpga:bmc_fw_version', + 'root_key': 'fpga:root_key', + 'revoked_key_ids': 'fpga:revoked_key_ids', + 'boot_page': 'fpga:boot_page', + 'bitstream_id': 'fpga:bitstream_id', + } + + _optional_fields = { + 'bmc_build_version', + 'bmc_fw_version', + 'root_key', + 'revoked_key_ids', + 'boot_page', + 'bitstream_id', } @base.remotable_classmethod diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/data/bitstream.bit b/sysinv/sysinv/sysinv/sysinv/tests/api/data/bitstream.bit new file mode 100644 index 0000000000..28d14454c3 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/data/bitstream.bit @@ -0,0 +1 @@ +123456789 diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_device_image.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_device_image.py new file mode 100644 index 0000000000..ec876c8f53 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_device_image.py @@ -0,0 +1,519 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Tests for the API /device_images/ methods. +""" + +import json +import mock +import os +from oslo_utils import uuidutils +from six.moves import http_client + +from sysinv.common import constants +from sysinv.common import device as dconstants + +from sysinv.tests.api import base +from sysinv.tests.db import base as dbbase +from sysinv.tests.db import utils as dbutils + + +class FakeConductorAPI(object): + + def __init__(self): + self.store_bitstream_file = mock.MagicMock() + self.delete_bitstream_file = mock.MagicMock() + + +class TestDeviceImage(base.FunctionalTest, dbbase.BaseHostTestCase): + # API_HEADERS are a generic header passed to most API calls + API_HEADERS = {'User-Agent': 'sysinv-test'} + + # API_PREFIX is the prefix for the URL + API_PREFIX = '/device_images' + + # RESULT_KEY is the python table key for the list of results + RESULT_KEY = 'device_images' + + # expected_api_fields are attributes that should be populated by + # an API query + expected_api_fields = ['id', + 'uuid', + 'bitstream_type', + 'pci_vendor', + 'pci_device', + 'bitstream_id', + 'key_signature', + 'revoke_key_id', + ] + + # hidden_api_fields are attributes that should not be populated by + # an API query + hidden_api_fields = [''] + + def setUp(self): + super(TestDeviceImage, self).setUp() + + # Mock the Conductor API + self.fake_conductor_api = FakeConductorAPI() + p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') + self.mock_conductor_api = p.start() + self.mock_conductor_api.return_value = self.fake_conductor_api + self.addCleanup(p.stop) + + def get_single_url(self, uuid): + return '%s/%s' % (self.API_PREFIX, uuid) + + +class TestListDeviceImage(TestDeviceImage): + def setUp(self): + super(TestListDeviceImage, self).setUp() + + def test_one(self): + device_image = dbutils.create_test_device_image( + bitstream_type=dconstants.BITSTREAM_TYPE_FUNCTIONAL, + pci_vendor='80ee', + pci_device='beef', + bitstream_id='12345', + ) + result = self.get_json('/device_images/%s' % device_image['uuid']) + + # Verify that the upgrade has the expected attributes + self.assertEqual(result['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(result['pci_vendor'], '80ee') + self.assertEqual(result['pci_device'], 'beef') + self.assertEqual(result['bitstream_id'], '12345') + + def test_list_all(self): + dbutils.create_test_device_image( + bitstream_type=dconstants.BITSTREAM_TYPE_FUNCTIONAL, + pci_vendor='80ee', + pci_device='beef', + bitstream_id='12345', + ) + data = self.get_json('/device_images') + self.assertEqual(1, len(data['device_images'])) + self.assertEqual(data['device_images'][0]['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(data['device_images'][0]['pci_vendor'], '80ee') + self.assertEqual(data['device_images'][0]['pci_device'], 'beef') + self.assertEqual(data['device_images'][0]['bitstream_id'], '12345') + + +class TestPostDeviceImage(TestDeviceImage, dbbase.ControllerHostTestCase): + + def test_create_functional_image(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': dconstants.BITSTREAM_TYPE_FUNCTIONAL, + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'bitstream_id': '12345', + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', + data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=False) + self.assertEqual(result.status_code, http_client.OK) + + # Verify that the images were downloaded + self.fake_conductor_api.store_bitstream_file.\ + assert_called_with(mock.ANY, mock.ANY) + + resp = json.loads(result.body) + self.assertIn('device_image', resp) + resp_dict = resp.get('device_image') + # Verify that the device image has the expected attributes + self.assertEqual(resp_dict['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(resp_dict['pci_vendor'], '80ee') + self.assertEqual(resp_dict['pci_device'], 'beef') + self.assertEqual(resp_dict['bitstream_id'], '12345') + + def test_create_root_key_image(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': dconstants.BITSTREAM_TYPE_ROOT_KEY, + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'key_signature': '12345', + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', + data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=False) + self.assertEqual(result.status_code, http_client.OK) + + # Verify that the images were downloaded + self.fake_conductor_api.store_bitstream_file.\ + assert_called_with(mock.ANY, mock.ANY) + + resp = json.loads(result.body) + self.assertIn('device_image', resp) + resp_dict = resp.get('device_image') + # Verify that the device image has the expected attributes + self.assertEqual(resp_dict['bitstream_type'], + dconstants.BITSTREAM_TYPE_ROOT_KEY) + self.assertEqual(resp_dict['pci_vendor'], '80ee') + self.assertEqual(resp_dict['pci_device'], 'beef') + self.assertEqual(resp_dict['key_signature'], '12345') + + def test_create_revoke_key_image(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': dconstants.BITSTREAM_TYPE_KEY_REVOCATION, + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'revoke_key_id': 12345, + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', + data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=False) + self.assertEqual(result.status_code, http_client.OK) + + # Verify that the images were downloaded + self.fake_conductor_api.store_bitstream_file.\ + assert_called_with(mock.ANY, mock.ANY) + + resp = json.loads(result.body) + self.assertIn('device_image', resp) + resp_dict = resp.get('device_image') + # Verify that the device image has the expected attributes + self.assertEqual(resp_dict['bitstream_type'], + dconstants.BITSTREAM_TYPE_KEY_REVOCATION) + self.assertEqual(resp_dict['pci_vendor'], '80ee') + self.assertEqual(resp_dict['pci_device'], 'beef') + self.assertEqual(resp_dict['revoke_key_id'], 12345) + + def test_create_functional_image_failure(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': dconstants.BITSTREAM_TYPE_FUNCTIONAL, + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'revoke_key_id': '12345', + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=True) + self.assertIn("bitstream_id is required for functional bitstream type", + str(result)) + + def test_create_root_key_image_failure(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': dconstants.BITSTREAM_TYPE_ROOT_KEY, + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'revoke_key_id': '12345', + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=True) + self.assertIn("key_signature is required for root key bitstream type", + str(result)) + + def test_create_revoke_key_image_failure(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': dconstants.BITSTREAM_TYPE_KEY_REVOCATION, + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'bitstream_id': '12345', + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=True) + self.assertIn("revoke_key_id is required for key revocation bitstream" + " type", str(result)) + + def test_create_bitstream_type_invalid(self): + # Test creation of device image + bitstream_file = os.path.join(os.path.dirname(__file__), "data", + 'bitstream.bit') + data = { + 'bitstream_type': 'wrong_type', + 'pci_vendor': '80ee', + 'pci_device': 'beef', + 'bitstream_id': '12345', + } + upload_file = [('file', bitstream_file)] + result = self.post_with_files('/device_images', data, + upload_files=upload_file, + headers=self.API_HEADERS, + expect_errors=True) + self.assertIn("Bitstream type wrong_type not supported", str(result)) + + +class TestPatch(TestDeviceImage): + def setUp(self): + super(TestPatch, self).setUp() + self.controller = dbutils.create_test_ihost( + id='1', + uuid=None, + forisystemid=self.system.id, + hostname='controller-0', + personality=constants.CONTROLLER, + subfunctions=constants.CONTROLLER, + invprovision=constants.PROVISIONED + ) + # Create a pci_device and fpga_device object + self.pci_device = dbutils.create_test_pci_devices( + host_id=self.controller.id, + pclass='Processing accelerators', + pclass_id='120000',) + self.fpga_device = dbutils.create_test_fpga_device( + host_id=self.controller.id, + pci_id=self.pci_device.id) + + # Create a device image + self.device_image = dbutils.create_test_device_image( + bitstream_type=dconstants.BITSTREAM_TYPE_FUNCTIONAL, + pci_vendor='80ee', + pci_device='beef', + bitstream_id='12345') + self.device_image2 = dbutils.create_test_device_image( + bitstream_type=dconstants.BITSTREAM_TYPE_FUNCTIONAL, + pci_vendor='80ee', + pci_device='beef', + bitstream_id='6789') + + def test_device_image_apply_all_hosts(self): + # Test applying device image to all hosts with fpga devices + + # Apply the device image + path = '/device_images/%s?action=apply' % self.device_image.uuid + response = self.patch_json(path, {}, + headers=self.API_HEADERS) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + self.assertEqual(response.json['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(response.json['pci_vendor'], '80ee') + self.assertEqual(response.json['pci_device'], 'beef') + self.assertEqual(response.json['bitstream_id'], '12345') + + # Verify that an entry of image to device mapping is updated + dev_img_state = self.dbapi.device_image_state_get_by_image_device( + self.device_image.id, self.pci_device.id) + self.assertEqual(dconstants.DEVICE_IMAGE_UPDATE_PENDING, + dev_img_state.status) + + # Verify that needs_firmware_update flag is updated in pci_device + pci_dev = self.dbapi.pci_device_get(self.pci_device.id) + self.assertEqual(pci_dev['needs_firmware_update'], True) + + def test_device_image_apply_invalid_image(self): + # Test applying device image with non-existing image + + # Apply the device image + path = '/device_images/%s?action=apply' % uuidutils.generate_uuid() + response = self.patch_json(path, {}, + headers=self.API_HEADERS, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertIn("image does not exist", + response.json['error_message']) + + def test_device_image_apply_with_label(self): + # Test applying device image to pci devices with specified label + + # Assign label to a device + self.post_json('/device_labels', + {'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'value1'}, + headers=self.API_HEADERS) + + # Apply the device image with label + path = '/device_images/%s?action=apply' % self.device_image.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + self.assertEqual(response.json['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(response.json['pci_vendor'], '80ee') + self.assertEqual(response.json['pci_device'], 'beef') + self.assertEqual(response.json['bitstream_id'], '12345') + self.assertEqual(response.json['applied_labels'], {'key1': 'value1'}) + + # Verify that the image to device mapping is updated + dev_img_state = self.dbapi.device_image_state_get_by_image_device( + self.device_image.id, self.pci_device.id) + self.assertEqual(dconstants.DEVICE_IMAGE_UPDATE_PENDING, + dev_img_state.status) + + # Verify that needs_firmware_update flag is updated in pci_device + pci_dev = self.dbapi.pci_device_get(self.pci_device.id) + self.assertEqual(pci_dev['needs_firmware_update'], True) + + def test_device_image_apply_invalid_label(self): + # Test applying device image with non-existing device label + + # Apply the device image + path = '/device_images/%s?action=apply' % self.device_image.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertIn("Device image apply failed: label key1=value1 does not" + " exist", response.json['error_message']) + + def test_device_image_apply_overwrite_functional(self): + # Test applying second device image with label + + # Assign label to a device + self.post_json('/device_labels', + {'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'value1'}, + headers=self.API_HEADERS) + + # Apply the device image with label + path = '/device_images/%s?action=apply' % self.device_image.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + + # Apply a second functional device image with label + path = '/device_images/%s?action=apply' % self.device_image2.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS, + expect_errors=True) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + + def test_device_image_remove_all_hosts(self): + # Test removing device image for all hosts with fpga devices + # Remove the device image + path = '/device_images/%s?action=remove' % self.device_image.uuid + response = self.patch_json(path, {}, + headers=self.API_HEADERS) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + self.assertEqual(response.json['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(response.json['pci_vendor'], '80ee') + self.assertEqual(response.json['pci_device'], 'beef') + self.assertEqual(response.json['bitstream_id'], '12345') + + # Verify that needs_firmware_update flag is updated in pci_device + pci_dev = self.dbapi.pci_device_get(self.pci_device.id) + self.assertEqual(pci_dev['needs_firmware_update'], False) + + def test_device_image_remove_by_label(self): + # Test removing device image by device label + + # Assign label to a device + self.post_json('/device_labels', + {'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'value1'}, + headers=self.API_HEADERS) + + # Apply the device image with label + path = '/device_images/%s?action=apply' % self.device_image.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + + # Remove the device image with label + path = '/device_images/%s?action=remove' % self.device_image.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + self.assertEqual(response.json['bitstream_type'], + dconstants.BITSTREAM_TYPE_FUNCTIONAL) + self.assertEqual(response.json['pci_vendor'], '80ee') + self.assertEqual(response.json['pci_device'], 'beef') + self.assertEqual(response.json['bitstream_id'], '12345') + + def test_device_image_remove_by_label_not_applied(self): + # Test removing device image by label where device label is not applied + + # Assign label to a device + self.post_json('/device_labels', + {'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'value1'}, + headers=self.API_HEADERS) + + # Remove the device image with label + path = '/device_images/%s?action=remove' % self.device_image.uuid + response = self.patch_json(path, {'key1': 'value1'}, + headers=self.API_HEADERS, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertIn("Device image %s not associated with label key1=value1" % + self.device_image.uuid, response.json['error_message']) + + +class TestDelete(TestDeviceImage): + + def test_delete(self): + # Test deleting a device image + + # Create the device image + device_image = dbutils.create_test_device_image( + bitstream_type=dconstants.BITSTREAM_TYPE_FUNCTIONAL, + pci_vendor='80ee', + pci_device='beef', + bitstream_id='12345') + + # Delete the device image + self.delete('/device_images/%s' % device_image.uuid, + headers={'User-Agent': 'sysinv-test'}) + + # Verify the device image no longer exists + response = self.get_json('/device_images/%s' % device_image.uuid, + expect_errors=True) + self.assertEqual(response.status_int, 404) + self.assertEqual(response.content_type, 'application/json') + self.assertTrue(response.json['error_message']) + + def test_delete_not_exist(self): + # Test deleting a device image + + # Delete the device image + uuid = uuidutils.generate_uuid() + response = self.delete('/device_images/%s' % uuid, + headers={'User-Agent': 'sysinv-test'}, + expect_errors=True) + self.assertEqual(response.status_int, 404) + self.assertEqual(response.content_type, 'application/json') + self.assertTrue(response.json['error_message']) + self.assertIn("Device image %s could not be found" % uuid, + response.json['error_message']) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_device_label.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_device_label.py new file mode 100644 index 0000000000..981b8895dc --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_device_label.py @@ -0,0 +1,143 @@ +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import json +from six.moves import http_client +from six.moves.urllib.parse import urlencode + +from sysinv.db import api as dbapi +from sysinv.tests.api import base +from sysinv.tests.db import base as dbbase +from sysinv.tests.db import utils as dbutils + + +class DeviceLabelTestCase(base.FunctionalTest, dbbase.ControllerHostTestCase): + def setUp(self): + super(DeviceLabelTestCase, self).setUp() + self.dbapi = dbapi.get_instance() + # Create a pci_device and fpga_device object + self.pci_device = dbutils.create_test_pci_devices( + host_id=self.host.id, + pclass='Processing accelerators', + pclass_id='120000',) + self.fpga_device = dbutils.create_test_fpga_device( + host_id=self.host.id, + pci_id=self.pci_device.id) + self.generic_labels = { + 'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'value1', + 'key2': 'value2' + } + + def _get_path(self, params=None): + path = '/device_labels' + + if params: + path += '?' + urlencode(params) + return path + + def validate_labels(self, input_data, response_data): + for t in response_data: + for k, v in t.items(): + if k in input_data.keys(): + self.assertEqual(v, input_data[k]) + + def assign_labels(self, input_data, parameters=None): + response = self.post_json('%s' % self._get_path(parameters), input_data) + self.assertEqual(http_client.OK, response.status_int) + return response + + def assign_labels_failure(self, input_data, parameters=None): + response = self.post_json('%s' % self._get_path(parameters), input_data, + expect_errors=True) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertTrue(response.json['error_message']) + + def get_device_labels(self): + response = self.get_json("/device_labels") + return response['device_labels'] + + +class DeviceLabelAssignTestCase(DeviceLabelTestCase): + def setUp(self): + super(DeviceLabelAssignTestCase, self).setUp() + + def test_create_device_labels(self): + self.assign_labels(self.generic_labels) + response_data = self.get_device_labels() + self.validate_labels(self.generic_labels, response_data) + + def test_overwrite_device_labels_success(self): + self.assign_labels(self.generic_labels) + + new_input_values = { + 'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'string1', + 'key2': 'string2' + } + self.assign_labels(new_input_values, parameters={'overwrite': True}) + response_data = self.get_device_labels() + self.validate_labels(new_input_values, response_data) + + def test_overwrite_device_labels_failure(self): + self.assign_labels(self.generic_labels) + + new_input_values = { + 'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'string1', + 'key2': 'string2' + } + # Default value should be overwrite=False + self.assign_labels_failure(new_input_values) + # Test explicit overwrite=False + self.assign_labels_failure(new_input_values, parameters={'overwrite': False}) + + # Labels should be unchanged from initial values + response_data = self.get_device_labels() + self.validate_labels(self.generic_labels, response_data) + + def test_create_validated_device_labels_success(self): + label1 = { + 'pcidevice_uuid': self.pci_device.uuid, + 'key1': 'value1', + } + self.assign_labels(label1) + label2 = { + 'pcidevice_uuid': self.pci_device.uuid, + 'key2': 'value2', + } + self.assign_labels(label2) + + input_data = {} + for input_label in [label1, label2]: + input_data.update(input_label) + + response_data = self.get_device_labels() + self.validate_labels(input_data, response_data) + + +class DeviceLabelRemoveTestCase(DeviceLabelTestCase): + def setUp(self): + super(DeviceLabelRemoveTestCase, self).setUp() + + def test_remove_device_labels(self): + # Assign labels to a device + response = self.assign_labels(self.generic_labels) + resp = json.loads(response.body) + self.assertIn('device_labels', resp) + resp_dict = resp.get('device_labels') + uuid = resp_dict[0]['uuid'] + + # Remove a label from the device + self.delete('/device_labels/%s' % uuid, + headers={'User-Agent': 'sysinv-test'}) + + # Verify the device label no longer exists + response = self.get_json('/device_labels/%s' % uuid, + expect_errors=True) + self.assertEqual(response.status_int, http_client.BAD_REQUEST) + self.assertEqual(response.content_type, 'application/json') + self.assertTrue(response.json['error_message']) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py b/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py index b7a11d0532..bf2ab99626 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py @@ -1327,6 +1327,38 @@ def create_test_pci_devices(**kw): return dbapi.pci_device_create(pci_devices['host_id'], pci_devices) +def get_test_fpga_device(**kw): + fpga_device = { + 'id': kw.get('id', 2345), + 'host_id': kw.get('host_id', 2), + 'pci_id': kw.get('pci_id', 2), + 'pciaddr': kw.get('pciaddr', '0000:00:02.0'), + 'bmc_build_version': kw.get('bmc_build_version'), + 'bmc_fw_version': kw.get('bmc_fw_version'), + 'root_key': kw.get('root_key'), + 'revoked_key_ids': kw.get('revoked_key_ids'), + 'boot_page': kw.get('boot_page'), + 'bitstream_id': kw.get('bitstream_id'), + 'needs_firmware_update': kw.get('needs_firmware_update', False), + 'status': kw.get('status'), + } + return fpga_device + + +def create_test_fpga_device(**kw): + """Create test fpga devices entry in DB and return FPGADevice DB object. + Function to be used to create test fpga device objects in the database. + :param kw: kwargs with overriding values for fpga device attributes. + :returns: Test FPGADevice DB object. + """ + fpga_device = get_test_fpga_device(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del fpga_device['id'] + dbapi = db_api.get_instance() + return dbapi.fpga_device_create(fpga_device['host_id'], fpga_device) + + def get_test_label(**kw): label = { 'host_id': kw.get('host_id'), @@ -1393,3 +1425,44 @@ def create_test_certificate(**kw): del certificate['id'] dbapi = db_api.get_instance() return dbapi.certificate_create(certificate) + + +# Create test device image object +def get_test_device_image(**kw): + device_image = { + 'id': kw.get('id'), + 'uuid': kw.get('uuid'), + 'bitstream_type': kw.get('bitstream_type'), + 'pci_vendor': kw.get('pci_vendor'), + 'pci_device': kw.get('pci_device'), + 'bitstream_id': kw.get('bitstream_id'), + 'key_signature': kw.get('key_signature'), + 'revoke_key_id': kw.get('revoke_key_id'), + 'name': kw.get('name'), + 'description': kw.get('description'), + 'version': kw.get('version'), + } + return device_image + + +def post_get_test_device_image(**kw): + device_image = get_test_device_image(**kw) + del device_image['id'] + del device_image['uuid'] + return device_image + + +def create_test_device_image(**kw): + """Create test device image in DB and return device_image object. + Function to be used to create test device image objects in the database. + :param kw: kwargs with overriding values for device_image's attributes. + :returns: Test device_image DB object. + """ + device_image = get_test_device_image(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in device_image: + del device_image['id'] + if 'uuid' in device_image: + del device_image['uuid'] + dbapi = db_api.get_instance() + return dbapi.deviceimage_create(device_image)