Add SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY environment variable support
I commonly see customer problems with dualstack environments where BMC's only have one IP stack. This change allows emulating this case. This change adds support for restricting virtual media image URLs based on IP family through the SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY environment variable. When set to "4" or "6", the emulator will only accept image URLs containing IPv4 or IPv6 addresses respectively, returning an error for mismatched IP families. URLs with hostnames are always allowed regardless of the setting, and if the variable is not set, no validation is performed to maintain backward compatibility. Change-Id: I3dab6f80eded8eddf04f823ea326f01655bfad8e Generated-By: Claude Code Signed-off-by: Dmitry Tantsur <dtantsur@protonmail.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
@@ -27,6 +28,46 @@ from sushy_tools.emulator.resources import base
|
||||
from sushy_tools import error
|
||||
|
||||
|
||||
def _validate_ip_family(image_url, required_ip_family):
|
||||
"""Validate that the IP address in the URL matches the required IP family.
|
||||
|
||||
:param image_url: URL to validate
|
||||
:param required_ip_family: Required IP family ('4' for IPv4, '6' for IPv6)
|
||||
:raises: BadRequest if the IP family doesn't match
|
||||
:returns: None if validation passes or URL contains hostname
|
||||
"""
|
||||
if not required_ip_family:
|
||||
return
|
||||
|
||||
if required_ip_family not in ('4', '6'):
|
||||
raise error.BadRequest(
|
||||
f"Invalid IP family configuration: {required_ip_family}. "
|
||||
"Must be '4' or '6'.")
|
||||
|
||||
parsed_url = urlparse.urlparse(image_url)
|
||||
host = parsed_url.hostname
|
||||
|
||||
if not host:
|
||||
return
|
||||
|
||||
# Try to parse as IP address
|
||||
try:
|
||||
ip_addr = ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
# Not an IP address, it's a hostname - skip validation
|
||||
return
|
||||
|
||||
# Check IP family
|
||||
if (required_ip_family == '4'
|
||||
and not isinstance(ip_addr, ipaddress.IPv4Address)):
|
||||
raise error.BadRequest(
|
||||
f"IPv4 address required but got IPv6 address: {host}")
|
||||
elif (required_ip_family == '6'
|
||||
and not isinstance(ip_addr, ipaddress.IPv6Address)):
|
||||
raise error.BadRequest(
|
||||
f"IPv6 address required but got IPv4 address: {host}")
|
||||
|
||||
|
||||
DeviceInfo = collections.namedtuple(
|
||||
'DeviceInfo',
|
||||
['image_name', 'image_url', 'inserted', 'write_protected',
|
||||
@@ -315,6 +356,12 @@ class StaticDriver(BaseDriver):
|
||||
:param write_protected: prevent write access the inserted media
|
||||
:raises: `FishyError` if image can't be manipulated
|
||||
"""
|
||||
# Validate IP family if configured
|
||||
required_ip_family = self._config.get(
|
||||
'SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY')
|
||||
if image_url and required_ip_family:
|
||||
_validate_ip_family(image_url, required_ip_family)
|
||||
|
||||
device_info = self._get_device(identity, device)
|
||||
verify_media_cert = device_info.get(
|
||||
'Verify',
|
||||
@@ -425,6 +472,12 @@ class OpenstackDriver(BaseDriver):
|
||||
:param write_protected: prevent write access the inserted media
|
||||
:raises: `FishyError` if image can't be manipulated
|
||||
"""
|
||||
# Validate IP family if configured
|
||||
required_ip_family = self._config.get(
|
||||
'SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY')
|
||||
if image_url and required_ip_family:
|
||||
_validate_ip_family(image_url, required_ip_family)
|
||||
|
||||
device_info = self._get_device(identity, device)
|
||||
verify_media_cert = device_info.get(
|
||||
'Verify',
|
||||
|
@@ -637,3 +637,178 @@ class OpenstackDriverTestCase(base.BaseTestCase):
|
||||
self.UUID, 'Cd')
|
||||
self.assertEqual('ouch', str(e))
|
||||
self.assertTrue(device_info['Inserted'])
|
||||
|
||||
|
||||
class IpFamilyValidationTestCase(base.BaseTestCase):
|
||||
"""Test IP family validation for virtual media."""
|
||||
|
||||
UUID = 'ZZZ-YYY-XXX'
|
||||
|
||||
CONFIG = {
|
||||
'SUSHY_EMULATOR_VMEDIA_DEVICES': {
|
||||
"Cd": {
|
||||
"Name": "Virtual CD",
|
||||
"MediaTypes": [
|
||||
"CD",
|
||||
"DVD"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
|
||||
def test_insert_image_ip_family_validation_ipv4_required_ipv6_provided(
|
||||
self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
# Create driver with IPv4 restriction
|
||||
config = self.CONFIG.copy()
|
||||
config['SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY'] = '4'
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.StaticDriver(config, mock.MagicMock())
|
||||
|
||||
# IPv6 URL should raise error when IPv4 is required
|
||||
self.assertRaises(error.BadRequest,
|
||||
driver.insert_image,
|
||||
self.UUID, 'Cd', 'http://[::1]/image.iso')
|
||||
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
|
||||
def test_insert_image_ip_family_validation_ipv6_required_ipv4_provided(
|
||||
self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
# Create driver with IPv6 restriction
|
||||
config = self.CONFIG.copy()
|
||||
config['SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY'] = '6'
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.StaticDriver(config, mock.MagicMock())
|
||||
|
||||
# IPv4 URL should raise error when IPv6 is required
|
||||
self.assertRaises(error.BadRequest,
|
||||
driver.insert_image,
|
||||
self.UUID, 'Cd', 'http://192.168.1.1/image.iso')
|
||||
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_image', autospec=True)
|
||||
def test_insert_image_ip_family_validation_hostname_allowed(
|
||||
self, mock_get_image, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
mock_get_image.return_value = ('image.iso', '/tmp/image.iso')
|
||||
|
||||
# Create driver with IPv4 restriction
|
||||
config = self.CONFIG.copy()
|
||||
config['SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY'] = '4'
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.StaticDriver(config, mock.MagicMock())
|
||||
|
||||
# Hostname should be allowed regardless of IP family restriction
|
||||
result = driver.insert_image(self.UUID, 'Cd',
|
||||
'http://example.com/image.iso')
|
||||
self.assertEqual('/tmp/image.iso', result)
|
||||
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_image', autospec=True)
|
||||
def test_insert_image_ip_family_validation_ipv4_allowed(
|
||||
self, mock_get_image, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
mock_get_image.return_value = ('image.iso', '/tmp/image.iso')
|
||||
|
||||
# Create driver with IPv4 restriction
|
||||
config = self.CONFIG.copy()
|
||||
config['SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY'] = '4'
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.StaticDriver(config, mock.MagicMock())
|
||||
|
||||
# IPv4 address should be allowed when IPv4 is required
|
||||
result = driver.insert_image(self.UUID, 'Cd',
|
||||
'http://192.168.1.1/image.iso')
|
||||
self.assertEqual('/tmp/image.iso', result)
|
||||
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_image', autospec=True)
|
||||
def test_insert_image_ip_family_validation_ipv6_allowed(
|
||||
self, mock_get_image, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
mock_get_image.return_value = ('image.iso', '/tmp/image.iso')
|
||||
|
||||
# Create driver with IPv6 restriction
|
||||
config = self.CONFIG.copy()
|
||||
config['SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY'] = '6'
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.StaticDriver(config, mock.MagicMock())
|
||||
|
||||
# IPv6 address should be allowed when IPv6 is required
|
||||
result = driver.insert_image(self.UUID, 'Cd',
|
||||
'http://[::1]/image.iso')
|
||||
self.assertEqual('/tmp/image.iso', result)
|
||||
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_device', autospec=True)
|
||||
@mock.patch.object(vmedia.StaticDriver, '_get_image', autospec=True)
|
||||
def test_insert_image_ip_family_validation_disabled(
|
||||
self, mock_get_image, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
mock_get_image.return_value = ('image.iso', '/tmp/image.iso')
|
||||
|
||||
# Create driver without IP family restriction
|
||||
config = self.CONFIG.copy()
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.StaticDriver(config, mock.MagicMock())
|
||||
|
||||
# Both IPv4 and IPv6 should be allowed when no restriction is set
|
||||
result = driver.insert_image(self.UUID, 'Cd',
|
||||
'http://192.168.1.1/image.iso')
|
||||
self.assertEqual('/tmp/image.iso', result)
|
||||
|
||||
result = driver.insert_image(self.UUID, 'Cd',
|
||||
'http://[::1]/image.iso')
|
||||
self.assertEqual('/tmp/image.iso', result)
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_openstack_driver_ip_family_validation_ipv4_required_ipv6(
|
||||
self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
# Create driver with IPv4 restriction
|
||||
config = {'SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY': '4'}
|
||||
novadriver = mock.Mock()
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.OpenstackDriver(config, mock.MagicMock(),
|
||||
novadriver)
|
||||
|
||||
# IPv6 URL should raise error when IPv4 is required
|
||||
self.assertRaises(error.BadRequest,
|
||||
driver.insert_image,
|
||||
self.UUID, 'Cd', 'http://[::1]/image.iso')
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_openstack_driver_ip_family_validation_ipv6_required_ipv4(
|
||||
self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
# Create driver with IPv6 restriction
|
||||
config = {'SUSHY_EMULATOR_VIRTUAL_MEDIA_IP_FAMILY': '6'}
|
||||
novadriver = mock.Mock()
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
driver = vmedia.OpenstackDriver(config, mock.MagicMock(),
|
||||
novadriver)
|
||||
|
||||
# IPv4 URL should raise error when IPv6 is required
|
||||
self.assertRaises(error.BadRequest,
|
||||
driver.insert_image,
|
||||
self.UUID, 'Cd', 'http://192.168.1.1/image.iso')
|
||||
|
Reference in New Issue
Block a user