Merge "Basic support for vmedia TLS certificates from version 1.4.0"
This commit is contained in:
commit
bb76f427af
@ -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.
|
||||
|
@ -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.
|
@ -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'])
|
||||
|
@ -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:
|
||||
|
18
sushy_tools/emulator/templates/certificate_collection.json
Normal file
18
sushy_tools/emulator/templates/certificate_collection.json
Normal 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."
|
||||
}
|
@ -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."
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user