Merge "Support segmented port ranges"

This commit is contained in:
Zuul
2025-11-21 21:50:22 +00:00
committed by Gerrit Code Review
4 changed files with 102 additions and 22 deletions
+5 -3
View File
@@ -54,12 +54,14 @@ opts = [
help=_('IP address of Socat service running on the host of '
'ironic conductor. Used only by Socat console.')),
cfg.StrOpt('port_range',
regex=r'^\d+:\d+$',
regex=r'^\d+:\d+(,\d+:\d+)*$',
sample_default='10000:20000',
help=_('A range of ports available to be used for the console '
'proxy service running on the host of ironic '
'conductor, in the form of <start>:<stop>. This option '
'is used by both Shellinabox and Socat console')),
'conductor, in the form of <start>:<stop> or '
'comma-separated ranges like '
'<start>:<stop>,<start>:<stop>. This option is used by '
'both Shellinabox and Socat console')),
]
+23 -17
View File
@@ -152,13 +152,18 @@ def make_persistent_password_file(path, password):
def _get_port_range():
config_range = CONF.console.port_range
ranges = []
start, stop = map(int, config_range.split(':'))
if start >= stop:
msg = _("[console]port_range should be in the "
"format <start>:<stop> and start < stop")
raise exception.InvalidParameterValue(msg)
return start, stop
for range_str in config_range.split(','):
start, stop = map(int, range_str.split(':'))
if start >= stop:
msg = _("[console]port_range should be in the "
"format <start>:<stop> or comma-separated ranges, "
"and start < stop for each range")
raise exception.InvalidParameterValue(msg)
ranges.append((start, stop))
return ranges
def _verify_port(port, host=None):
@@ -189,21 +194,22 @@ def _verify_port(port, host=None):
def acquire_port(host=None):
"""Returns a free TCP port on current host.
Find and returns a free TCP port in the range
Find and returns a free TCP port in the range(s)
of 'CONF.console.port_range'.
"""
start, stop = _get_port_range()
ranges = _get_port_range()
for port in range(start, stop):
if port in ALLOCATED_PORTS:
continue
try:
_verify_port(port, host=host)
ALLOCATED_PORTS.add(port)
return port
except exception.Conflict:
pass
for start, stop in ranges:
for port in range(start, stop):
if port in ALLOCATED_PORTS:
continue
try:
_verify_port(port, host=host)
ALLOCATED_PORTS.add(port)
return port
except exception.Conflict:
pass
raise exception.NoFreeIPMITerminalPorts(host=CONF.host)
@@ -695,14 +695,26 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
def test_valid_console_port_range(self):
self.config(port_range='10000:20000', group='console')
start, stop = console_utils._get_port_range()
self.assertEqual((start, stop), (10000, 20000))
ranges = console_utils._get_port_range()
self.assertEqual(ranges, [(10000, 20000)])
def test_valid_console_port_range_segmented(self):
self.config(port_range='1000:1100,2000:2500,3000:3100',
group='console')
ranges = console_utils._get_port_range()
self.assertEqual(ranges, [(1000, 1100),
(2000, 2500), (3000, 3100)])
def test_invalid_console_port_range(self):
self.config(port_range='20000:10000', group='console')
self.assertRaises(exception.InvalidParameterValue,
console_utils._get_port_range)
def test_invalid_console_port_range_segmented(self):
self.config(port_range='1000:1100,2500:2000', group='console')
self.assertRaises(exception.InvalidParameterValue,
console_utils._get_port_range)
@mock.patch.object(console_utils, 'ALLOCATED_PORTS', autospec=True)
@mock.patch.object(console_utils, '_verify_port', autospec=True)
def test_allocate_port_success(self, mock_verify, mock_ports):
@@ -736,6 +748,59 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
verify_calls = [mock.call(p, host=None) for p in range(10000, 10005)]
mock_verify.assert_has_calls(verify_calls)
@mock.patch.object(console_utils, 'ALLOCATED_PORTS', autospec=True)
@mock.patch.object(console_utils, '_verify_port', autospec=True)
def test_allocate_port_segmented_range_first_range(self, mock_verify,
mock_ports):
self.config(port_range='1000:1001,2000:2001', group='console')
port = console_utils.acquire_port()
mock_verify.assert_called_once_with(1000, host=None)
self.assertEqual(port, 1000)
mock_ports.add.assert_called_once_with(1000)
@mock.patch.object(console_utils, 'ALLOCATED_PORTS', autospec=True)
@mock.patch.object(console_utils, '_verify_port', autospec=True)
def test_allocate_port_segmented_range_second_range(self, mock_verify,
mock_ports):
self.config(port_range='1000:1001,2000:2001', group='console')
# Port 1000 is already allocated
mock_ports.__contains__ = mock.Mock(side_effect=lambda x: x == 1000)
mock_verify.side_effect = (None,)
port = console_utils.acquire_port()
mock_verify.assert_called_once_with(2000, host=None)
self.assertEqual(port, 2000)
mock_ports.add.assert_called_once_with(2000)
@mock.patch.object(console_utils, 'ALLOCATED_PORTS', autospec=True)
@mock.patch.object(console_utils, '_verify_port', autospec=True)
def test_allocate_port_segmented_range_exhausted_first(self, mock_verify,
mock_ports):
self.config(port_range='1000:1002,2000:2002', group='console')
# First range ports are in ALLOCATED_PORTS
mock_ports.__contains__ = mock.Mock(
side_effect=lambda x: x in (1000, 1001))
mock_verify.side_effect = (None,)
port = console_utils.acquire_port()
# Should skip first range and get port from second range
mock_verify.assert_called_once_with(2000, host=None)
self.assertEqual(port, 2000)
mock_ports.add.assert_called_once_with(2000)
@mock.patch.object(console_utils, 'ALLOCATED_PORTS', autospec=True)
@mock.patch.object(console_utils, '_verify_port', autospec=True)
def test_allocate_port_segmented_range_no_free_ports(self, mock_verify,
mock_ports):
self.config(port_range='1000:1002,2000:2002', group='console')
mock_verify.side_effect = exception.Conflict
self.assertRaises(exception.NoFreeIPMITerminalPorts,
console_utils.acquire_port)
# Should try all ports in both ranges
verify_calls = (
[mock.call(p, host=None) for p in range(1000, 1002)]
+ [mock.call(p, host=None) for p in range(2000, 2002)])
mock_verify.assert_has_calls(verify_calls)
@mock.patch.object(socket, 'socket', autospec=True)
def test__verify_port_default(self, mock_socket):
self.config(host='localhost.localdomain')
@@ -0,0 +1,7 @@
---
features:
- |
The ``[console]port_range`` configuration option now supports segmented
port ranges, allowing multiple non-consecutive port ranges to be specified
as comma-separated values. For example, ``1000:1100,2000:2500`` can be
used instead of requiring a single consecutive range.