Handle HTTP errors when downloading virtual media

Currently the emulator happily downloads the error page from nginx and
tries to connect it to libvirt. This change catches errors and returns
an error to the caller with either Bad Request (for 4xx codes upstream)
or Bad Gateway (for 5xx codes upstream).

Change-Id: I67fadb6b68c0fade9cfd122fe9a36dc6c0d34959
This commit is contained in:
Dmitry Tantsur
2021-08-19 19:05:22 +02:00
parent 4d519bea0e
commit 158eb3c866
4 changed files with 89 additions and 29 deletions

View File

@@ -0,0 +1,5 @@
---
fixes:
- |
Fixes handling HTTP errors on downloading virtual media files. Error codes
are no longer silently ignored.

View File

@@ -127,6 +127,30 @@ class StaticDriver(base.DriverBase):
device_info.get('Inserted', False),
device_info.get('WriteProtected', False))
def _write_from_response(self, image_url, rsp, tmp_file):
with open(tmp_file.name, 'wb') as fl:
for chunk in rsp.iter_content(chunk_size=8192):
if chunk:
fl.write(chunk)
local_file = None
content_dsp = rsp.headers.get('content-disposition')
if content_dsp:
local_file = re.findall('filename="(.+)"', content_dsp)
if local_file:
local_file = local_file[0]
if not local_file:
parsed_url = urlparse.urlparse(image_url)
local_file = os.path.basename(parsed_url.path)
if not local_file:
local_file = 'image.iso'
return local_file
def insert_image(self, identity, device, image_url,
inserted=True, write_protected=True):
"""Upload, remove or insert virtual media
@@ -144,42 +168,34 @@ class StaticDriver(base.DriverBase):
'SUSHY_EMULATOR_VMEDIA_VERIFY_SSL', True)
try:
with tempfile.NamedTemporaryFile(
mode='w+b', delete=False) as tmp_file:
with requests.get(image_url,
stream=True,
verify=verify_media_cert) as rsp:
if rsp.status_code >= 400:
self._logger.error(
'Failed fetching image from URL %s: '
'got HTTP error %s:\n%s',
image_url, rsp.status_code, rsp.text)
target_code = 502 if rsp.status_code >= 500 else 400
raise error.FishyError(
"Cannot download virtual media: got error %s "
"from the server" % rsp.status_code,
code=target_code)
with requests.get(image_url,
stream=True,
verify=verify_media_cert) as rsp:
with open(tmp_file.name, 'wb') as fl:
for chunk in rsp.iter_content(chunk_size=8192):
if chunk:
fl.write(chunk)
local_file = None
content_dsp = rsp.headers.get('content-disposition')
if content_dsp:
local_file = re.findall('filename="(.+)"', content_dsp)
if local_file:
local_file = local_file[0]
if not local_file:
parsed_url = urlparse.urlparse(image_url)
local_file = os.path.basename(parsed_url.path)
if not local_file:
local_file = 'image.iso'
with tempfile.NamedTemporaryFile(
mode='w+b', delete=False) as tmp_file:
local_file = self._write_from_response(image_url,
rsp, tmp_file)
temp_dir = tempfile.mkdtemp(
dir=os.path.dirname(tmp_file.name))
local_file_path = os.path.join(temp_dir, local_file)
os.rename(tmp_file.name, local_file_path)
except error.FishyError as ex:
msg = 'Failed fetching image from URL %s: %s' % (image_url, ex)
self._logger.error(msg)
raise # leave the original error intact (code, etc)
except Exception as ex:
msg = 'Failed fetching image from URL %s: %s' % (image_url, ex)
self._logger.exception(msg)

View File

@@ -17,6 +17,10 @@
class FishyError(Exception):
"""Create generic sushy-tools exception object"""
def __init__(self, msg='Unknown error', code=500):
super().__init__(msg)
self.code = code
class AliasAccessError(FishyError):
"""Node access attempted via an alias, not UUID"""

View File

@@ -19,6 +19,7 @@ from unittest import mock
from oslotest import base
from sushy_tools.emulator.resources import vmedia
from sushy_tools import error
class StaticDriverTestCase(base.BaseTestCase):
@@ -90,6 +91,7 @@ class StaticDriverTestCase(base.BaseTestCase):
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,
@@ -125,6 +127,7 @@ class StaticDriverTestCase(base.BaseTestCase):
mock_tmp_file.name = 'alphabet.soup'
mock_rsp = mock_requests.get.return_value.__enter__.return_value
mock_rsp.headers = {}
mock_rsp.status_code = 200
local_file = self.test_driver.insert_image(
self.UUID, 'Cd', 'http://fish.it/red.iso', inserted=True,
@@ -159,6 +162,7 @@ class StaticDriverTestCase(base.BaseTestCase):
mock_tmp_file.name = 'alphabet.soup'
mock_rsp = mock_requests.get.return_value.__enter__.return_value
mock_rsp.headers = {}
mock_rsp.status_code = 200
full_url = 'http://[::2]:80/redfish/boot-abc?filename=tmp.iso'
local_file = self.test_driver.insert_image(
@@ -196,6 +200,7 @@ class StaticDriverTestCase(base.BaseTestCase):
mock_rsp.headers = {
'content-disposition': 'attachment; filename="fish.iso"'
}
mock_rsp.status_code = 200
ssl_conf_key = 'SUSHY_EMULATOR_VMEDIA_VERIFY_SSL'
default_ssl_verify = self.test_driver._config.get(ssl_conf_key)
@@ -220,6 +225,36 @@ class StaticDriverTestCase(base.BaseTestCase):
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_fail(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 = 401
self.assertRaises(error.FishyError,
self.test_driver.insert_image,
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)
mock_open.assert_not_called()
self.assertEqual({}, device_info)
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
@mock.patch.object(vmedia.os, 'unlink', autospec=True)
def test_eject_image(self, mock_unlink, mock_get_device):