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:
Sahid Orentino Ferdjaoui 2014-08-25 12:37:11 +00:00
parent b26a281ebe
commit 5cfe77a2f4
3 changed files with 260 additions and 0 deletions

115
nova/console/serial.py Normal file
View 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()

View File

@ -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")

View 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))