console: add serial console module
Adds new module console/serial.py to handle configuration and TCP ports management for the feature serial-ports. Co-Authored-By: Vladan Popovic <vpopovic@redhat.com> Co-Authored-By: Ian Wells <iawells@cisco.com> Co-Authored-By: Sushma Korati <sushma_korati@persistent.co.in> DocImpact: new group of option 'serial_console' Partial-Implements: blueprint serial-ports Change-Id: Icb7f3569a29a5fab9aaa3a2d441f5fe4e5b55b9f
This commit is contained in:
parent
b26a281ebe
commit
5cfe77a2f4
115
nova/console/serial.py
Normal file
115
nova/console/serial.py
Normal file
@ -0,0 +1,115 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Serial consoles module."""
|
||||
|
||||
import socket
|
||||
|
||||
from oslo.config import cfg
|
||||
import six.moves
|
||||
|
||||
from nova import exception
|
||||
from nova.i18n import _LW
|
||||
from nova.openstack.common import log as logging
|
||||
from nova import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
ALLOCATED_PORTS = set() # in-memory set of already allocated ports
|
||||
SERIAL_LOCK = 'serial-lock'
|
||||
DEFAULT_PORT_RANGE = '10000:20000'
|
||||
|
||||
serial_opts = [
|
||||
cfg.BoolOpt('enabled',
|
||||
default=False,
|
||||
help='Enable serial console related features'),
|
||||
cfg.StrOpt('port_range',
|
||||
default=DEFAULT_PORT_RANGE,
|
||||
help='Range of TCP ports to use for serial ports '
|
||||
'on compute hosts'),
|
||||
cfg.StrOpt('base_url',
|
||||
default='http://127.0.0.1:6083/',
|
||||
help='Location of serial console proxy.'),
|
||||
cfg.StrOpt('listen',
|
||||
default='127.0.0.1',
|
||||
help=('IP address on which instance serial console '
|
||||
'should listen')),
|
||||
cfg.StrOpt('proxyclient_address',
|
||||
default='127.0.0.1',
|
||||
help=('The address to which proxy clients '
|
||||
'(like nova-serialproxy) should connect')),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(serial_opts, group='serial_console')
|
||||
|
||||
# TODO(sahid): Add a method to initialize ALOCATED_PORTS with the
|
||||
# already binded TPC port(s). (cf from danpb: list all running guests and
|
||||
# query the XML in libvirt driver to find out the TCP port(s) it uses).
|
||||
|
||||
|
||||
@utils.synchronized(SERIAL_LOCK)
|
||||
def acquire_port(host):
|
||||
"""Returns a free TCP port on host.
|
||||
|
||||
Find and returns a free TCP port on 'host' in the range
|
||||
of 'CONF.serial_console.port_range'.
|
||||
"""
|
||||
|
||||
start, stop = _get_port_range()
|
||||
|
||||
for port in six.moves.range(start, stop):
|
||||
if (host, port) in ALLOCATED_PORTS:
|
||||
continue
|
||||
try:
|
||||
_verify_port(host, port)
|
||||
ALLOCATED_PORTS.add((host, port))
|
||||
return port
|
||||
except exception.SocketPortInUseException as e:
|
||||
LOG.warn(e)
|
||||
|
||||
raise exception.SocketPortRangeExhaustedException(host=host)
|
||||
|
||||
|
||||
@utils.synchronized(SERIAL_LOCK)
|
||||
def release_port(host, port):
|
||||
"""Release TCP port to be used next time."""
|
||||
ALLOCATED_PORTS.discard((host, port))
|
||||
|
||||
|
||||
def _get_port_range():
|
||||
config_range = CONF.serial_console.port_range
|
||||
try:
|
||||
start, stop = map(int, config_range.split(':'))
|
||||
if start >= stop:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
LOG.warn(_LW("serial_console.port_range should be <num>:<num>. "
|
||||
"Given value %(port_range)s could not be parsed. "
|
||||
"Taking the default port range %(default)s."),
|
||||
{'port_range': config_range,
|
||||
'default': DEFAULT_PORT_RANGE})
|
||||
start, stop = map(int, DEFAULT_PORT_RANGE.split(':'))
|
||||
return start, stop
|
||||
|
||||
|
||||
def _verify_port(host, port):
|
||||
s = socket.socket()
|
||||
try:
|
||||
s.bind((host, port))
|
||||
except socket.error as e:
|
||||
raise exception.SocketPortInUseException(
|
||||
host=host, port=port, error=e)
|
||||
finally:
|
||||
s.close()
|
@ -1704,3 +1704,11 @@ class InvalidHostname(Invalid):
|
||||
|
||||
class NumaTopologyNotFound(NotFound):
|
||||
msg_fmt = _("Instance %(instance_uuid)s does not specify a NUMA topology")
|
||||
|
||||
|
||||
class SocketPortRangeExhaustedException(NovaException):
|
||||
msg_fmt = _("Not able to acquire a free port for %(host)s")
|
||||
|
||||
|
||||
class SocketPortInUseException(NovaException):
|
||||
msg_fmt = _("Not able to bind %(host)s:%(port)d, %(error)s")
|
||||
|
137
nova/tests/console/test_serial.py
Normal file
137
nova/tests/console/test_serial.py
Normal file
@ -0,0 +1,137 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for Serial Console."""
|
||||
|
||||
import socket
|
||||
|
||||
import mock
|
||||
import six.moves
|
||||
|
||||
from nova.console import serial
|
||||
from nova import exception
|
||||
from nova import test
|
||||
|
||||
|
||||
class SerialTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(SerialTestCase, self).setUp()
|
||||
serial.ALLOCATED_PORTS = set()
|
||||
|
||||
def test_get_port_range(self):
|
||||
start, stop = serial._get_port_range()
|
||||
self.assertEqual(10000, start)
|
||||
self.assertEqual(20000, stop)
|
||||
|
||||
def test_get_port_range_customized(self):
|
||||
self.flags(port_range='30000:40000', group='serial_console')
|
||||
start, stop = serial._get_port_range()
|
||||
self.assertEqual(30000, start)
|
||||
self.assertEqual(40000, stop)
|
||||
|
||||
def test_get_port_range_bad_range(self):
|
||||
self.flags(port_range='40000:30000', group='serial_console')
|
||||
start, stop = serial._get_port_range()
|
||||
self.assertEqual(10000, start)
|
||||
self.assertEqual(20000, stop)
|
||||
|
||||
def test_get_port_range_not_numeric(self):
|
||||
self.flags(port_range='xxx:yyy', group='serial_console')
|
||||
start, stop = serial._get_port_range()
|
||||
self.assertEqual(10000, start)
|
||||
self.assertEqual(20000, stop)
|
||||
|
||||
def test_get_port_range_invalid_syntax(self):
|
||||
self.flags(port_range='10:20:30', group='serial_console')
|
||||
start, stop = serial._get_port_range()
|
||||
self.assertEqual(10000, start)
|
||||
self.assertEqual(20000, stop)
|
||||
|
||||
@mock.patch('socket.socket')
|
||||
def test_verify_port(self, fake_socket):
|
||||
s = mock.MagicMock()
|
||||
fake_socket.return_value = s
|
||||
|
||||
serial._verify_port('127.0.0.1', 10)
|
||||
|
||||
s.bind.assert_called_once_with(('127.0.0.1', 10))
|
||||
|
||||
@mock.patch('socket.socket')
|
||||
def test_verify_port_in_use(self, fake_socket):
|
||||
s = mock.MagicMock()
|
||||
s.bind.side_effect = socket.error()
|
||||
fake_socket.return_value = s
|
||||
|
||||
self.assertRaises(
|
||||
exception.SocketPortInUseException,
|
||||
serial._verify_port, '127.0.0.1', 10)
|
||||
|
||||
s.bind.assert_called_once_with(('127.0.0.1', 10))
|
||||
|
||||
@mock.patch('nova.console.serial._verify_port', lambda x, y: None)
|
||||
def test_acquire_port(self):
|
||||
start, stop = 15, 20
|
||||
self.flags(
|
||||
port_range='%d:%d' % (start, stop),
|
||||
group='serial_console')
|
||||
|
||||
for port in six.moves.range(start, stop):
|
||||
self.assertEqual(port, serial.acquire_port('127.0.0.1'))
|
||||
|
||||
for port in six.moves.range(start, stop):
|
||||
self.assertEqual(port, serial.acquire_port('127.0.0.2'))
|
||||
|
||||
self.assertTrue(10, len(serial.ALLOCATED_PORTS))
|
||||
|
||||
@mock.patch('nova.console.serial._verify_port')
|
||||
def test_acquire_port_in_use(self, fake_verify_port):
|
||||
def port_10000_already_used(host, port):
|
||||
if port == 10000 and host == '127.0.0.1':
|
||||
raise exception.SocketPortInUseException(
|
||||
port=port,
|
||||
host=host,
|
||||
error="already in use")
|
||||
fake_verify_port.side_effect = port_10000_already_used
|
||||
|
||||
self.assertEqual(10001, serial.acquire_port('127.0.0.1'))
|
||||
self.assertEqual(10000, serial.acquire_port('127.0.0.2'))
|
||||
|
||||
self.assertNotIn(('127.0.0.1', 10000), serial.ALLOCATED_PORTS)
|
||||
self.assertIn(('127.0.0.1', 10001), serial.ALLOCATED_PORTS)
|
||||
self.assertIn(('127.0.0.2', 10000), serial.ALLOCATED_PORTS)
|
||||
|
||||
@mock.patch('nova.console.serial._verify_port')
|
||||
def test_acquire_port_not_ble_to_bind_at_any_port(self, fake_verify_port):
|
||||
start, stop = 15, 20
|
||||
self.flags(
|
||||
port_range='%d:%d' % (start, stop),
|
||||
group='serial_console')
|
||||
|
||||
fake_verify_port.side_effect = (
|
||||
exception.SocketPortRangeExhaustedException(host='127.0.0.1'))
|
||||
|
||||
self.assertRaises(
|
||||
exception.SocketPortRangeExhaustedException,
|
||||
serial.acquire_port, '127.0.0.1')
|
||||
|
||||
def test_release_port(self):
|
||||
serial.ALLOCATED_PORTS.add(('127.0.0.1', 100))
|
||||
serial.ALLOCATED_PORTS.add(('127.0.0.2', 100))
|
||||
self.assertEqual(2, len(serial.ALLOCATED_PORTS))
|
||||
|
||||
serial.release_port('127.0.0.1', 100)
|
||||
self.assertEqual(1, len(serial.ALLOCATED_PORTS))
|
||||
|
||||
serial.release_port('127.0.0.2', 100)
|
||||
self.assertEqual(0, len(serial.ALLOCATED_PORTS))
|
Loading…
Reference in New Issue
Block a user