Merge "Basic support for vmedia TLS certificates from version 1.4.0"

This commit is contained in:
Zuul 2021-09-08 12:54:53 +00:00 committed by Gerrit Code Review
commit bb76f427af
9 changed files with 233 additions and 20 deletions

View File

@ -99,7 +99,7 @@ SUSHY_EMULATOR_VMEDIA_DEVICES = {
# Instruct the virtual media insertion not to verify the SSL certificate
# when retrieving the image.
SUSHY_EMULATOR_VMEDIA_VERIFY_SSL = True
SUSHY_EMULATOR_VMEDIA_VERIFY_SSL = False
# This map contains statically configured Redfish Storage resource linked
# up with the Systems resource, keyed by the UUIDs of the Systems.

View File

@ -0,0 +1,9 @@
---
features:
- |
Supports reading and changing ``VerifyCertificate`` in ``VirtualMedia``
resources.
upgrade:
- |
The default value of ``SUSHY_EMULATOR_VMEDIA_VERIFY_SSL`` has been changed
to ``False`` to match the actual bare metal hardware.

View File

@ -434,9 +434,66 @@ def virtual_media_resource(identity, device):
write_protected=device_info.write_protected,
username=device_info.username,
password=device_info.password,
verify_certificate=device_info.verify,
)
@app.route(
'/redfish/v1/Managers/<identity>/VirtualMedia/<device>',
methods=['PATCH'])
@returns_json
def virtual_media_patch(identity, device):
if not flask.request.json:
raise error.BadRequest("Empty or malformed patch")
app.logger.debug('Updating virtual media %s at manager "%s"',
device, identity)
verify = flask.request.json.get('VerifyCertificate')
if verify is not None:
if not isinstance(verify, bool):
raise error.BadRequest("VerifyCertificate must be a boolean")
try:
app.vmedia.update_device_info(identity, device, verify=verify)
except error.FishyError as ex:
app.logger.warning(
'Virtual media %s at manager %s error: '
'%s', device, identity, ex)
raise error.NotFound("Virtual media device not found")
return '', 204
else:
raise error.BadRequest("Empty or malformed patch")
@app.route(
'/redfish/v1/Managers/<identity>/VirtualMedia/<device>/Certificates',
methods=['GET'])
@returns_json
def virtual_media_certificates(identity, device):
location = \
f'/redfish/v1/Managers/{identity}/VirtualMedia/{device}/Certificates'
return flask.render_template(
'certificate_collection.json',
location=location,
# TODO(dtantsur): implement
certificates=[],
)
@app.route(
'/redfish/v1/Managers/<identity>/VirtualMedia/<device>/Certificates',
methods=['POST'])
@returns_json
def virtual_media_add_certificate(identity, device):
if not flask.request.json:
raise error.BadRequest("Empty or malformed certificate")
# TODO(dtantsur): implement
raise error.NotSupportedError("Not implemented")
@app.route('/redfish/v1/Managers/<identity>/VirtualMedia/<device>'
'/Actions/VirtualMedia.InsertMedia',
methods=['POST'])

View File

@ -29,7 +29,7 @@ from sushy_tools import error
DeviceInfo = collections.namedtuple(
'DeviceInfo',
['image_name', 'image_url', 'inserted', 'write_protected',
'username', 'password'])
'username', 'password', 'verify'])
class StaticDriver(base.DriverBase):
@ -135,7 +135,20 @@ class StaticDriver(base.DriverBase):
device_info.get('Inserted', False),
device_info.get('WriteProtected', False),
device_info.get('UserName', ''),
device_info.get('Password', ''))
device_info.get('Password', ''),
device_info.get('Verify', False))
def update_device_info(self, identity, device, verify=False):
"""Update the virtual media device
:param identity: parent resource ID
:param device: device name
:param verify: new value for VerifyCertificate
:raises: `error.FishyError`
"""
device_info = self._get_device(identity, device)
device_info['Verify'] = verify
self._devices[(identity, device)] = device_info
def _write_from_response(self, image_url, rsp, tmp_file):
with open(tmp_file.name, 'wb') as fl:
@ -175,8 +188,11 @@ class StaticDriver(base.DriverBase):
:raises: `FishyError` if image can't be manipulated
"""
device_info = self._get_device(identity, device)
verify_media_cert = self._config.get(
'SUSHY_EMULATOR_VMEDIA_VERIFY_SSL', True)
verify_media_cert = device_info.get(
'Verify',
# NOTE(dtantsur): it's de facto standard for Redfish to default
# to no certificate validation.
self._config.get('SUSHY_EMULATOR_VMEDIA_VERIFY_SSL', False))
auth = (username, password) if (username and password) else None
try:

View File

@ -0,0 +1,18 @@
{
"@odata.type": "#CertificateCollection.CertificateCollection",
"Name": "Certificate Collection",
"Members@odata.count": {{ certificates|length }},
"Members": [
{% for item in certificates %}
{
"@odata.id": {{ "%s/%s"|format(location, item)|tojson }}
}{% if not loop.last %},{% endif %}
{% endfor %}
],
"@Redfish.SupportedCertificates": [
"PEM"
],
"Oem": {},
"@odata.id": {{ location|tojson }},
"@Redfish.Copyright": "Copyright 2014-2021 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -1,5 +1,5 @@
{
"@odata.type": "#VirtualMedia.v1_3_0.VirtualMedia",
"@odata.type": "#VirtualMedia.v1_4_0.VirtualMedia",
"Id": {{ device|string|tojson }},
"Name": {{ name|string|tojson }},
"MediaTypes": [
@ -23,6 +23,10 @@
},
"UserName": {{ username|string|tojson }},
"Password": "{{ '******' if password else '' }}",
"Certificates": {
"@odata.id": {{ "/redfish/v1/Managers/%s/VirtualMedia/%s/Certificates"|format(identity, device)|tojson }}
},
"VerifyCertificate": {{ verify_certificate|tojson }},
"@odata.context": "/redfish/v1/$metadata#VirtualMedia.VirtualMedia",
"@odata.id": {{ "/redfish/v1/Managers/%s/VirtualMedia/%s"|format(identity, device)|string|tojson }},
"@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."

View File

@ -28,3 +28,17 @@ class AliasAccessError(FishyError):
class NotSupportedError(FishyError):
"""Feature not supported by resource driver"""
class NotFound(FishyError):
"""Entity not found."""
def __init__(self, msg, code=404):
super().__init__(msg, code)
class BadRequest(FishyError):
"""Malformed request."""
def __init__(self, msg, code=400):
super().__init__(msg, code)

View File

@ -69,9 +69,17 @@ class StaticDriverTestCase(base.BaseTestCase):
def test_get_device_image_info(self):
dev_info = self.test_driver.get_device_image_info(
self.UUID, 'Cd')
expected = ('', '', False, False, '', '')
expected = ('', '', False, False, '', '', False)
self.assertEqual(expected, dev_info)
def test_update_device_info(self):
dev_info = self.test_driver.get_device_image_info(self.UUID, 'Cd')
self.assertFalse(dev_info.verify)
self.test_driver.update_device_info(self.UUID, 'Cd', verify=True)
dev_info = self.test_driver.get_device_image_info(self.UUID, 'Cd')
self.assertTrue(dev_info.verify)
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
@mock.patch.object(builtins, 'open', autospec=True)
@mock.patch.object(vmedia.os, 'rename', autospec=True)
@ -99,7 +107,7 @@ class StaticDriverTestCase(base.BaseTestCase):
self.assertEqual('/alphabet/soup/fish.iso', local_file)
mock_requests.get.assert_called_once_with(
'http://fish.it/red.iso', stream=True, verify=True, auth=None)
'http://fish.it/red.iso', stream=True, verify=False, auth=None)
mock_open.assert_called_once_with(mock.ANY, 'wb')
mock_rename.assert_called_once_with(
'alphabet.soup', '/alphabet/soup/fish.iso')
@ -138,7 +146,7 @@ class StaticDriverTestCase(base.BaseTestCase):
self.assertEqual('/alphabet/soup/fish.iso', local_file)
mock_requests.get.assert_called_once_with(
'http://fish.it/red.iso', stream=True, verify=True,
'http://fish.it/red.iso', stream=True, verify=False,
auth=('Admin', 'Secret'))
mock_open.assert_called_once_with(mock.ANY, 'wb')
mock_rename.assert_called_once_with(
@ -177,7 +185,7 @@ class StaticDriverTestCase(base.BaseTestCase):
self.assertEqual('/alphabet/soup/red.iso', local_file)
mock_requests.get.assert_called_once_with(
'http://fish.it/red.iso', stream=True, verify=True, auth=None)
'http://fish.it/red.iso', stream=True, verify=False, auth=None)
mock_open.assert_called_once_with(mock.ANY, 'wb')
mock_rename.assert_called_once_with(
'alphabet.soup', '/alphabet/soup/red.iso')
@ -213,7 +221,7 @@ class StaticDriverTestCase(base.BaseTestCase):
self.assertEqual('/alphabet/soup/boot-abc', local_file)
mock_requests.get.assert_called_once_with(full_url, stream=True,
verify=True, auth=None)
verify=False, auth=None)
mock_open.assert_called_once_with(mock.ANY, 'wb')
mock_rename.assert_called_once_with(
'alphabet.soup', '/alphabet/soup/boot-abc')
@ -227,9 +235,9 @@ class StaticDriverTestCase(base.BaseTestCase):
@mock.patch.object(vmedia.os, 'rename', autospec=True)
@mock.patch.object(vmedia, 'tempfile', autospec=True)
@mock.patch.object(vmedia, 'requests', autospec=True)
def test_insert_image_no_verify_ssl(self, mock_requests, mock_tempfile,
mock_rename, mock_open,
mock_get_device):
def test_insert_image_verify_ssl(self, mock_requests, mock_tempfile,
mock_rename, mock_open,
mock_get_device):
device_info = {}
mock_get_device.return_value = device_info
@ -247,8 +255,7 @@ class StaticDriverTestCase(base.BaseTestCase):
ssl_conf_key = 'SUSHY_EMULATOR_VMEDIA_VERIFY_SSL'
default_ssl_verify = self.test_driver._config.get(ssl_conf_key)
try:
self.test_driver._config[ssl_conf_key] = (
False)
self.test_driver._config[ssl_conf_key] = True
local_file = self.test_driver.insert_image(
self.UUID, 'Cd', 'https://fish.it/red.iso', inserted=True,
write_protected=False)
@ -257,7 +264,46 @@ class StaticDriverTestCase(base.BaseTestCase):
self.assertEqual('/alphabet/soup/fish.iso', local_file)
mock_requests.get.assert_called_once_with(
'https://fish.it/red.iso', stream=True, verify=False, auth=None)
'https://fish.it/red.iso', stream=True, verify=True, auth=None)
mock_open.assert_called_once_with(mock.ANY, 'wb')
mock_rename.assert_called_once_with(
'alphabet.soup', '/alphabet/soup/fish.iso')
self.assertEqual('fish.iso', device_info['Image'])
self.assertTrue(device_info['Inserted'])
self.assertFalse(device_info['WriteProtected'])
self.assertEqual(local_file, device_info['_local_file'])
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
@mock.patch.object(builtins, 'open', autospec=True)
@mock.patch.object(vmedia.os, 'rename', autospec=True)
@mock.patch.object(vmedia, 'tempfile', autospec=True)
@mock.patch.object(vmedia, 'requests', autospec=True)
def test_insert_image_verify_ssl_changed(self, mock_requests,
mock_tempfile,
mock_rename, mock_open,
mock_get_device):
device_info = {'Verify': True}
mock_get_device.return_value = device_info
mock_tempfile.mkdtemp.return_value = '/alphabet/soup'
mock_tempfile.gettempdir.return_value = '/tmp'
mock_tmp_file = (mock_tempfile.NamedTemporaryFile
.return_value.__enter__.return_value)
mock_tmp_file.name = 'alphabet.soup'
mock_rsp = mock_requests.get.return_value.__enter__.return_value
mock_rsp.headers = {
'content-disposition': 'attachment; filename="fish.iso"'
}
mock_rsp.status_code = 200
local_file = self.test_driver.insert_image(
self.UUID, 'Cd', 'https://fish.it/red.iso', inserted=True,
write_protected=False)
self.assertEqual('/alphabet/soup/fish.iso', local_file)
mock_requests.get.assert_called_once_with(
'https://fish.it/red.iso', stream=True, verify=True, auth=None)
mock_open.assert_called_once_with(mock.ANY, 'wb')
mock_rename.assert_called_once_with(
'alphabet.soup', '/alphabet/soup/fish.iso')
@ -293,7 +339,7 @@ class StaticDriverTestCase(base.BaseTestCase):
self.UUID, 'Cd', 'http://fish.it/red.iso',
inserted=True, write_protected=False)
mock_requests.get.assert_called_once_with(
'http://fish.it/red.iso', stream=True, auth=None, verify=True)
'http://fish.it/red.iso', stream=True, auth=None, verify=False)
mock_open.assert_not_called()
self.assertEqual({}, device_info)

View File

@ -493,7 +493,7 @@ class VirtualMediaTestCase(EmulatorTestCase):
vmedia_mock.get_device_media_types.return_value = [
'CD', 'DVD']
vmedia_mock.get_device_image_info.return_value = vmedia.DeviceInfo(
'image-of-a-fish', 'fishy.iso', True, True, '', '')
'image-of-a-fish', 'fishy.iso', True, True, '', '', False)
response = self.app.get(
'/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid)
@ -507,6 +507,10 @@ class VirtualMediaTestCase(EmulatorTestCase):
self.assertTrue(response.json['WriteProtected'])
self.assertEqual('', response.json['UserName'])
self.assertEqual('', response.json['Password'])
self.assertFalse(response.json['VerifyCertificate'])
self.assertEqual(
'/redfish/v1/Managers/%s/VirtualMedia/CD/Certificates' % self.uuid,
response.json['Certificates']['@odata.id'])
def test_virtual_media_with_auth(self, managers_mock, vmedia_mock):
vmedia_mock = vmedia_mock.return_value
@ -514,7 +518,8 @@ class VirtualMediaTestCase(EmulatorTestCase):
vmedia_mock.get_device_media_types.return_value = [
'CD', 'DVD']
vmedia_mock.get_device_image_info.return_value = vmedia.DeviceInfo(
'image-of-a-fish', 'fishy.iso', True, True, 'Admin', 'Secret')
'image-of-a-fish', 'fishy.iso', True, True, 'Admin', 'Secret',
False)
response = self.app.get(
'/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid)
@ -528,6 +533,7 @@ class VirtualMediaTestCase(EmulatorTestCase):
self.assertTrue(response.json['WriteProtected'])
self.assertEqual('Admin', response.json['UserName'])
self.assertEqual('******', response.json['Password'])
self.assertFalse(response.json['VerifyCertificate'])
def test_virtual_media_not_found(self, managers_mock, vmedia_mock):
vmedia_mock.return_value.get_device_name.side_effect = error.FishyError
@ -537,6 +543,39 @@ class VirtualMediaTestCase(EmulatorTestCase):
self.assertEqual(404, response.status_code)
def test_virtual_media_update(self, managers_mock, vmedia_mock):
response = self.app.patch(
'/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid,
json={'VerifyCertificate': True})
self.assertEqual(204, response.status_code)
vmedia_mock = vmedia_mock.return_value
vmedia_mock.update_device_info.assert_called_once_with(
self.uuid, 'CD', verify=True)
def test_virtual_media_update_not_found(self, managers_mock, vmedia_mock):
vmedia_mock = vmedia_mock.return_value
vmedia_mock.update_device_info.side_effect = error.FishyError
response = self.app.patch(
'/redfish/v1/Managers/%s/VirtualMedia/DVD-ROM' % self.uuid,
json={'VerifyCertificate': True})
self.assertEqual(404, response.status_code)
def test_virtual_media_update_invalid(self, managers_mock, vmedia_mock):
response = self.app.patch(
'/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid,
json={'VerifyCertificate': 'banana'})
self.assertEqual(400, response.status_code)
def test_virtual_media_update_empty(self, managers_mock, vmedia_mock):
response = self.app.patch(
'/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid)
self.assertEqual(400, response.status_code)
def test_virtual_media_insert(self, managers_mock, vmedia_mock):
response = self.app.post(
'/redfish/v1/Managers/%s/VirtualMedia/CD/Actions/'
@ -558,6 +597,16 @@ class VirtualMediaTestCase(EmulatorTestCase):
vmedia_mock.return_value.eject_image.called_once_with('CD')
def test_virtual_media_certificates(self, managers_mock, vmedia_mock):
response = self.app.get(
'/redfish/v1/Managers/%s/VirtualMedia/CD/Certificates' % self.uuid)
self.assertEqual(200, response.status_code, response.json)
self.assertEqual(0, response.json['Members@odata.count'])
self.assertEqual([], response.json['Members'])
self.assertEqual(['PEM'],
response.json['@Redfish.SupportedCertificates'])
@patch_resource('systems')
class StorageTestCase(EmulatorTestCase):