From af76d4dcea74efce27ba36ef719e4f0b1337d727 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 27 Jul 2021 15:06:29 +0200 Subject: [PATCH] Support credentials for virtual media Also update virtual media version to 1.3.0 and fix the default for write protected. Change-Id: Ib398f06a11442a1a2dd0a6883c3aeddf8aa0a4b1 --- .../vmedia-credentials-0f6e1f539bd94d14.yaml | 5 ++ sushy_tools/emulator/main.py | 25 +++++---- sushy_tools/emulator/resources/vmedia.py | 31 ++++++++--- .../emulator/templates/virtual_media.json | 6 ++- .../unit/emulator/resources/test_vmedia.py | 54 ++++++++++++++++--- sushy_tools/tests/unit/emulator/test_main.py | 30 +++++++++-- 6 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/vmedia-credentials-0f6e1f539bd94d14.yaml diff --git a/releasenotes/notes/vmedia-credentials-0f6e1f539bd94d14.yaml b/releasenotes/notes/vmedia-credentials-0f6e1f539bd94d14.yaml new file mode 100644 index 00000000..99e0c985 --- /dev/null +++ b/releasenotes/notes/vmedia-credentials-0f6e1f539bd94d14.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The emulator now supports providing ``UserName`` and ``Password`` for + virtual media. diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index 79e6303b..991da20d 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -411,9 +411,7 @@ def virtual_media_resource(identity, device): media_types = app.vmedia.get_device_media_types( identity, device) - (image_name, image_url, inserted, - write_protected) = app.vmedia.get_device_image_info( - identity, device) + device_info = app.vmedia.get_device_image_info(identity, device) except error.FishyError as ex: app.logger.warning( @@ -430,10 +428,12 @@ def virtual_media_resource(identity, device): device=device, name=device_name, media_types=media_types, - image_url=image_url, - image_name=image_name, - inserted=inserted, - write_protected=write_protected + image_url=device_info.image_url, + image_name=device_info.image_name, + inserted=device_info.inserted, + write_protected=device_info.write_protected, + username=device_info.username, + password=device_info.password, ) @@ -444,7 +444,13 @@ def virtual_media_resource(identity, device): def virtual_media_insert(identity, device): image = flask.request.json.get('Image') inserted = flask.request.json.get('Inserted', True) - write_protected = flask.request.json.get('WriteProtected', False) + write_protected = flask.request.json.get('WriteProtected', True) + username = flask.request.json.get('UserName', '') + password = flask.request.json.get('Password', '') + + if (not username and password) or (username and not password): + message = "UserName and Password must be passed together" + return flask.render_template('error.json', message=message), 400 manager = app.managers.get_manager(identity) systems = app.managers.get_managed_systems(manager) @@ -453,7 +459,8 @@ def virtual_media_insert(identity, device): return '', 204 image_path = app.vmedia.insert_image( - identity, device, image, inserted, write_protected) + identity, device, image, inserted, write_protected, + username=username, password=password) for system in systems: try: diff --git a/sushy_tools/emulator/resources/vmedia.py b/sushy_tools/emulator/resources/vmedia.py index c509c45b..c9b93171 100644 --- a/sushy_tools/emulator/resources/vmedia.py +++ b/sushy_tools/emulator/resources/vmedia.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import os import re import tempfile @@ -25,6 +26,12 @@ from sushy_tools.emulator.resources import base from sushy_tools import error +DeviceInfo = collections.namedtuple( + 'DeviceInfo', + ['image_name', 'image_url', 'inserted', 'write_protected', + 'username', 'password']) + + class StaticDriver(base.DriverBase): """Redfish virtual media simulator.""" @@ -116,16 +123,19 @@ class StaticDriver(base.DriverBase): :param identity: parent resource ID :param device: device name - :returns: a `tuple` of: image name, image path, `True` is media is - inserted, `True` if media is write-protected + :returns: a `DeviceInfo` with: image name, image path, + `True` is media is inserted, `True` if media is write-protected, + user name and password :raises: `error.FishyError` """ device_info = self._get_device(identity, device) - return (device_info.get('ImageName', ''), - device_info.get('Image', ''), - device_info.get('Inserted', False), - device_info.get('WriteProtected', False)) + return DeviceInfo(device_info.get('ImageName', ''), + device_info.get('Image', ''), + device_info.get('Inserted', False), + device_info.get('WriteProtected', False), + device_info.get('UserName', ''), + device_info.get('Password', '')) def _write_from_response(self, image_url, rsp, tmp_file): with open(tmp_file.name, 'wb') as fl: @@ -152,7 +162,8 @@ class StaticDriver(base.DriverBase): return local_file def insert_image(self, identity, device, image_url, - inserted=True, write_protected=True): + inserted=True, write_protected=True, + username=None, password=None): """Upload, remove or insert virtual media :param identity: parent resource ID @@ -166,10 +177,12 @@ class StaticDriver(base.DriverBase): device_info = self._get_device(identity, device) verify_media_cert = self._config.get( 'SUSHY_EMULATOR_VMEDIA_VERIFY_SSL', True) + auth = (username, password) if (username and password) else None try: with requests.get(image_url, stream=True, + auth=auth, verify=verify_media_cert) as rsp: if rsp.status_code >= 400: self._logger.error( @@ -208,6 +221,8 @@ class StaticDriver(base.DriverBase): device_info['Image'] = local_file device_info['Inserted'] = inserted device_info['WriteProtected'] = write_protected + device_info['UserName'] = username or '' + device_info['Password'] = password or '' device_info['_local_file'] = local_file_path self._devices.update({(identity, device): device_info}) @@ -227,6 +242,8 @@ class StaticDriver(base.DriverBase): device_info['ImageName'] = '' device_info['Inserted'] = False device_info['WriteProtected'] = False + device_info['UserName'] = '' + device_info['Password'] = '' self._devices.update({(identity, device): device_info}) diff --git a/sushy_tools/emulator/templates/virtual_media.json b/sushy_tools/emulator/templates/virtual_media.json index 94badefa..a726d693 100644 --- a/sushy_tools/emulator/templates/virtual_media.json +++ b/sushy_tools/emulator/templates/virtual_media.json @@ -1,5 +1,5 @@ { - "@odata.type": "#VirtualMedia.v1_1_0.VirtualMedia", + "@odata.type": "#VirtualMedia.v1_3_0.VirtualMedia", "Id": {{ device|string|tojson }}, "Name": {{ name|string|tojson }}, "MediaTypes": [ @@ -21,7 +21,9 @@ }, "Oem": {} }, + "UserName": {{ username|string|tojson }}, + "Password": "{{ '******' if password else '' }}", "@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." -} \ No newline at end of file +} diff --git a/sushy_tools/tests/unit/emulator/resources/test_vmedia.py b/sushy_tools/tests/unit/emulator/resources/test_vmedia.py index 476dc678..9e887540 100644 --- a/sushy_tools/tests/unit/emulator/resources/test_vmedia.py +++ b/sushy_tools/tests/unit/emulator/resources/test_vmedia.py @@ -69,7 +69,7 @@ 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, '', '') self.assertEqual(expected, dev_info) @mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True) @@ -99,7 +99,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=True, auth=None) mock_open.assert_called_once_with(mock.ANY, 'wb') mock_rename.assert_called_once_with( 'alphabet.soup', '/alphabet/soup/fish.iso') @@ -107,6 +107,48 @@ class StaticDriverTestCase(base.BaseTestCase): self.assertEqual('fish.iso', device_info['Image']) self.assertTrue(device_info['Inserted']) self.assertFalse(device_info['WriteProtected']) + self.assertEqual('', device_info['UserName']) + self.assertEqual('', device_info['Password']) + 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_auth(self, mock_requests, mock_tempfile, mock_rename, + mock_open, mock_get_device): + device_info = {} + 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', 'http://fish.it/red.iso', inserted=True, + write_protected=False, username='Admin', password='Secret') + + 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=('Admin', 'Secret')) + 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('Admin', device_info['UserName']) + self.assertEqual('Secret', device_info['Password']) self.assertEqual(local_file, device_info['_local_file']) @mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True) @@ -135,7 +177,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) + 'http://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/red.iso') @@ -171,7 +213,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) + verify=True, auth=None) mock_open.assert_called_once_with(mock.ANY, 'wb') mock_rename.assert_called_once_with( 'alphabet.soup', '/alphabet/soup/boot-abc') @@ -215,7 +257,7 @@ 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) + 'https://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') @@ -251,7 +293,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, verify=True) + 'http://fish.it/red.iso', stream=True, auth=None, verify=True) mock_open.assert_not_called() self.assertEqual({}, device_info) diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index a19ab535..1e9d7b83 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -16,6 +16,7 @@ from unittest import mock from oslotest import base from sushy_tools.emulator import main +from sushy_tools.emulator.resources import vmedia from sushy_tools import error @@ -491,19 +492,42 @@ class VirtualMediaTestCase(EmulatorTestCase): vmedia_mock.get_device_name.return_value = 'CD' vmedia_mock.get_device_media_types.return_value = [ 'CD', 'DVD'] - vmedia_mock.get_device_image_info.return_value = [ - 'image-of-a-fish', 'fishy.iso', True, True] + vmedia_mock.get_device_image_info.return_value = vmedia.DeviceInfo( + 'image-of-a-fish', 'fishy.iso', True, True, '', '') response = self.app.get( '/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid) - self.assertEqual(200, response.status_code) + self.assertEqual(200, response.status_code, response.json) self.assertEqual('CD', response.json['Id']) self.assertEqual(['CD', 'DVD'], response.json['MediaTypes']) self.assertEqual('fishy.iso', response.json['Image']) self.assertEqual('image-of-a-fish', response.json['ImageName']) self.assertTrue(response.json['Inserted']) self.assertTrue(response.json['WriteProtected']) + self.assertEqual('', response.json['UserName']) + self.assertEqual('', response.json['Password']) + + def test_virtual_media_with_auth(self, managers_mock, vmedia_mock): + vmedia_mock = vmedia_mock.return_value + vmedia_mock.get_device_name.return_value = 'CD' + 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') + + response = self.app.get( + '/redfish/v1/Managers/%s/VirtualMedia/CD' % self.uuid) + + self.assertEqual(200, response.status_code, response.json) + self.assertEqual('CD', response.json['Id']) + self.assertEqual(['CD', 'DVD'], response.json['MediaTypes']) + self.assertEqual('fishy.iso', response.json['Image']) + self.assertEqual('image-of-a-fish', response.json['ImageName']) + self.assertTrue(response.json['Inserted']) + self.assertTrue(response.json['WriteProtected']) + self.assertEqual('Admin', response.json['UserName']) + self.assertEqual('******', response.json['Password']) def test_virtual_media_not_found(self, managers_mock, vmedia_mock): vmedia_mock.return_value.get_device_name.side_effect = error.FishyError