Merge "Support segmented port ranges"
This commit is contained in:
@@ -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')),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user