virt: setup TCP chardevice in libvirt driver

Setup the tcp chardevice in the libvirt driver based on the
configuration (if the serial consoles are enabled) and disable the
logging to a file since we have an interactive console.

A new image property is introduced: hw_serial_port_count
A new flavor extra spec is introduced: hw:serial_port_count

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
Partial-Implements: blueprint serial-ports
Change-Id: Ic6d24e27d61f600a90ed8fccb525d8b82725903e
This commit is contained in:
Sahid Orentino Ferdjaoui 2014-08-25 12:37:52 +00:00
parent 8a23aad0b4
commit 9cf0c413d4
5 changed files with 358 additions and 8 deletions

View File

@ -1717,3 +1717,13 @@ class SocketPortRangeExhaustedException(NovaException):
class SocketPortInUseException(NovaException):
msg_fmt = _("Not able to bind %(host)s:%(port)d, %(error)s")
class ImageSerialPortNumberInvalid(Invalid):
msg_fmt = _("Number of serial ports '%(num_ports)s' specified in "
"'%(property)s' isn't valid.")
class ImageSerialPortNumberExceedFlavorValue(Invalid):
msg_fmt = _("Forbidden to exceed flavor value of number of serial "
"ports passed in image meta.")

View File

@ -1529,6 +1529,221 @@ class LibvirtConnTestCase(test.TestCase,
self.assertEqual(cfg.devices[5].type, "spice")
self.assertEqual(cfg.devices[6].type, "qxl")
@mock.patch('nova.console.serial.acquire_port')
def test_get_guest_config_serial_console(self, acquire_port):
self.flags(enabled=True, group='serial_console')
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
instance_ref = db.instance_create(self.context, self.test_instance)
disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance_ref)
acquire_port.return_value = 11111
cfg = conn._get_guest_config(instance_ref, [], {}, disk_info)
self.assertEqual(7, len(cfg.devices))
self.assertIsInstance(cfg.devices[0],
vconfig.LibvirtConfigGuestDisk)
self.assertIsInstance(cfg.devices[1],
vconfig.LibvirtConfigGuestDisk)
self.assertIsInstance(cfg.devices[2],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[3],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[4],
vconfig.LibvirtConfigGuestInput)
self.assertIsInstance(cfg.devices[5],
vconfig.LibvirtConfigGuestGraphics)
self.assertIsInstance(cfg.devices[6],
vconfig.LibvirtConfigGuestVideo)
self.assertEqual("tcp", cfg.devices[2].type)
self.assertEqual(11111, cfg.devices[2].listen_port)
def test_get_guest_config_serial_console_through_flavor(self):
self.flags(enabled=True, group='serial_console')
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
fake_flavor = objects.Flavor.get_by_id(
self.context, self.test_instance['instance_type_id'])
fake_flavor.extra_specs = {'hw:serial_port_count': 3}
instance_ref = db.instance_create(self.context, self.test_instance)
disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance_ref)
with mock.patch.object(objects.Flavor, 'get_by_id',
return_value=fake_flavor):
cfg = conn._get_guest_config(instance_ref, [], {}, disk_info)
self.assertEqual(9, len(cfg.devices))
self.assertIsInstance(cfg.devices[0],
vconfig.LibvirtConfigGuestDisk)
self.assertIsInstance(cfg.devices[1],
vconfig.LibvirtConfigGuestDisk)
self.assertIsInstance(cfg.devices[2],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[3],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[4],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[5],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[6],
vconfig.LibvirtConfigGuestInput)
self.assertIsInstance(cfg.devices[7],
vconfig.LibvirtConfigGuestGraphics)
self.assertIsInstance(cfg.devices[8],
vconfig.LibvirtConfigGuestVideo)
self.assertEqual("tcp", cfg.devices[2].type)
self.assertEqual("tcp", cfg.devices[3].type)
self.assertEqual("tcp", cfg.devices[4].type)
def test_get_guest_config_serial_console_through_invalid_flavor(self):
self.flags(enabled=True, group='serial_console')
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
fake_flavor = objects.Flavor.get_by_id(
self.context, self.test_instance['instance_type_id'])
fake_flavor.extra_specs = {'hw:serial_port_count': "a"}
instance_ref = db.instance_create(self.context, self.test_instance)
disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance_ref)
with mock.patch.object(objects.Flavor, 'get_by_id',
return_value=fake_flavor):
self.assertRaises(
exception.ImageSerialPortNumberInvalid,
conn._get_guest_config, instance_ref, [], {}, disk_info)
def test_get_guest_config_serial_console_image_meta_and_flavor(self):
self.flags(enabled=True, group='serial_console')
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
fake_flavor = objects.Flavor.get_by_id(
self.context, self.test_instance['instance_type_id'])
fake_flavor.extra_specs = {'hw:serial_port_count': 4}
image_meta = {"properties": {"hw_serial_port_count": "3"}}
instance_ref = db.instance_create(self.context, self.test_instance)
disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance_ref)
with mock.patch.object(objects.Flavor, 'get_by_id',
return_value=fake_flavor):
cfg = conn._get_guest_config(instance_ref, [], image_meta,
disk_info)
self.assertEqual(9, len(cfg.devices), cfg.devices)
self.assertIsInstance(cfg.devices[0],
vconfig.LibvirtConfigGuestDisk)
self.assertIsInstance(cfg.devices[1],
vconfig.LibvirtConfigGuestDisk)
self.assertIsInstance(cfg.devices[2],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[3],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[4],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[5],
vconfig.LibvirtConfigGuestSerial)
self.assertIsInstance(cfg.devices[6],
vconfig.LibvirtConfigGuestInput)
self.assertIsInstance(cfg.devices[7],
vconfig.LibvirtConfigGuestGraphics)
self.assertIsInstance(cfg.devices[8],
vconfig.LibvirtConfigGuestVideo)
self.assertEqual("tcp", cfg.devices[2].type)
self.assertEqual("tcp", cfg.devices[3].type)
self.assertEqual("tcp", cfg.devices[4].type)
def test_get_guest_config_serial_console_through_invalid_img_meta(self):
self.flags(enabled=True, group='serial_console')
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
instance_ref = db.instance_create(self.context, self.test_instance)
disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance_ref)
image_meta = {"properties": {"hw_serial_port_count": "fail"}}
self.assertRaises(
exception.ImageSerialPortNumberInvalid,
conn._get_guest_config, instance_ref, [], image_meta, disk_info)
@mock.patch('nova.console.serial.acquire_port')
def test_get_guest_config_serial_console_through_port_rng_exhausted(
self, acquire_port):
self.flags(enabled=True, group='serial_console')
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
instance_ref = db.instance_create(self.context, self.test_instance)
disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance_ref)
acquire_port.side_effect = exception.SocketPortRangeExhaustedException(
'127.0.0.1')
self.assertRaises(
exception.SocketPortRangeExhaustedException,
conn._get_guest_config, instance_ref, [], {}, disk_info)
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._lookup_by_name')
def test_get_serial_ports_from_instance(self, _lookup_by_name):
i = self._test_get_serial_ports_from_instance(_lookup_by_name)
self.assertEqual([
('127.0.0.1', 100),
('127.0.0.1', 101),
('127.0.0.2', 100),
('127.0.0.2', 101)], list(i))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._lookup_by_name')
def test_get_serial_ports_from_instance_bind_only(self, _lookup_by_name):
i = self._test_get_serial_ports_from_instance(
_lookup_by_name, mode='bind')
self.assertEqual([
('127.0.0.1', 101),
('127.0.0.2', 100)], list(i))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._lookup_by_name')
def test_get_serial_ports_from_instance_connect_only(self,
_lookup_by_name):
i = self._test_get_serial_ports_from_instance(
_lookup_by_name, mode='connect')
self.assertEqual([
('127.0.0.1', 100),
('127.0.0.2', 101)], list(i))
def _test_get_serial_ports_from_instance(self, _lookup_by_name, mode=None):
xml = """
<domain type='kvm'>
<devices>
<serial type="tcp">
<source host="127.0.0.1" service="100" mode="connect"/>
</serial>
<serial type="tcp">
<source host="127.0.0.1" service="101" mode="bind"/>
</serial>
<serial type="tcp">
<source host="127.0.0.2" service="100" mode="bind"/>
</serial>
<serial type="tcp">
<source host="127.0.0.2" service="101" mode="connect"/>
</serial>
</devices>
</domain>"""
dom = mock.MagicMock()
dom.XMLDesc.return_value = xml
_lookup_by_name.return_value = dom
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
return conn._get_serial_ports_from_instance(
{'name': 'fake_instance'}, mode=mode)
def test_get_guest_config_with_type_xen(self):
self.flags(vnc_enabled=True)
self.flags(virt_type='xen',

View File

@ -1077,3 +1077,43 @@ class NUMATopologyTest(test.NoDBTestCase):
for exp_cell, got_cell in zip(expected.cells, got.cells):
self.assertNUMACellMatches(exp_cell, got_cell)
class NumberOfSerialPortsTest(test.NoDBTestCase):
def test_flavor(self):
flavor = FakeFlavor(8, 2048, {"hw:serial_port_count": 3})
num_ports = hw.get_number_of_serial_ports(flavor, None)
self.assertEqual(3, num_ports)
def test_image_meta(self):
flavor = FakeFlavor(8, 2048, {})
image_meta = {"properties": {"hw_serial_port_count": 2}}
num_ports = hw.get_number_of_serial_ports(flavor, image_meta)
self.assertEqual(2, num_ports)
def test_flavor_invalid_value(self):
flavor = FakeFlavor(8, 2048, {"hw:serial_port_count": 'foo'})
image_meta = {"properties": {}}
self.assertRaises(exception.ImageSerialPortNumberInvalid,
hw.get_number_of_serial_ports,
flavor, image_meta)
def test_image_meta_invalid_value(self):
flavor = FakeFlavor(8, 2048, {})
image_meta = {"properties": {"hw_serial_port_count": 'bar'}}
self.assertRaises(exception.ImageSerialPortNumberInvalid,
hw.get_number_of_serial_ports,
flavor, image_meta)
def test_image_meta_smaller_than_flavor(self):
flavor = FakeFlavor(8, 2048, {"hw:serial_port_count": 3})
image_meta = {"properties": {"hw_serial_port_count": 2}}
num_ports = hw.get_number_of_serial_ports(flavor, image_meta)
self.assertEqual(2, num_ports)
def test_flavor_smaller_than_image_meta(self):
flavor = FakeFlavor(8, 2048, {"hw:serial_port_count": 3})
image_meta = {"properties": {"hw_serial_port_count": 4}}
self.assertRaises(exception.ImageSerialPortNumberExceedFlavorValue,
hw.get_number_of_serial_ports,
flavor, image_meta)

View File

@ -140,6 +140,52 @@ def format_cpu_spec(cpuset, allow_ranges=True):
return ",".join(str(id) for id in sorted(cpuset))
def get_number_of_serial_ports(flavor, image_meta):
"""Get the number of serial consoles from the flavor or image
:param flavor: Flavor object to read extra specs from
:param image_meta: Image object to read image metadata from
If flavor extra specs is not set, then any image meta value is permitted.
If flavour extra specs *is* set, then this provides the default serial
port count. The image meta is permitted to override the extra specs, but
*only* with a lower value. ie
- flavor hw:serial_port_count=4
VM gets 4 serial ports
- flavor hw:serial_port_count=4 and image hw_serial_port_count=2
VM gets 2 serial ports
- image hw_serial_port_count=6
VM gets 6 serial ports
- flavor hw:serial_port_count=4 and image hw_serial_port_count=6
Abort guest boot - forbidden to exceed flavor value
:returns: number of serial ports
"""
def get_number(obj, property):
num_ports = obj.get(property)
if num_ports is not None:
try:
num_ports = int(num_ports)
except ValueError:
raise exception.ImageSerialPortNumberInvalid(
num_ports=num_ports, property=property)
return num_ports
image_meta_prop = (image_meta or {}).get('properties', {})
flavor_num_ports = get_number(flavor.extra_specs, "hw:serial_port_count")
image_num_ports = get_number(image_meta_prop, "hw_serial_port_count")
if (flavor_num_ports and image_num_ports) is not None:
if image_num_ports > flavor_num_ports:
raise exception.ImageSerialPortNumberExceedFlavorValue()
return image_num_ports
return flavor_num_ports or image_num_ports or 1
class VirtCPUTopology(object):
def __init__(self, sockets, cores, threads):

View File

@ -55,6 +55,7 @@ from nova.compute import power_state
from nova.compute import task_states
from nova.compute import utils as compute_utils
from nova.compute import vm_mode
from nova.console import serial as serial_console
from nova import context as nova_context
from nova import exception
from nova.i18n import _
@ -242,6 +243,9 @@ CONF.import_opt('server_proxyclient_address', 'nova.spice', group='spice')
CONF.import_opt('vcpu_pin_set', 'nova.virt.hardware')
CONF.import_opt('vif_plugging_is_fatal', 'nova.virt.driver')
CONF.import_opt('vif_plugging_timeout', 'nova.virt.driver')
CONF.import_opt('enabled', 'nova.console.serial', group='serial_console')
CONF.import_opt('proxyclient_address', 'nova.console.serial',
group='serial_console')
DEFAULT_FIREWALL_DRIVER = "%s.%s" % (
libvirt_firewall.__name__,
@ -1120,6 +1124,26 @@ class LibvirtDriver(driver.ComputeDriver):
if CONF.libvirt.images_type == 'rbd':
self._cleanup_rbd(instance)
if CONF.serial_console.enabled:
for host, port in self._get_serial_ports_from_instance(instance):
serial_console.release_port(host=host, port=port)
def _get_serial_ports_from_instance(self, instance, mode=None):
"""Returns an iterator over serial port(s) configured on instance.
:param mode: Should be a value in (None, bind, connect)
"""
virt_dom = self._lookup_by_name(instance['name'])
xml = virt_dom.XMLDesc(0)
tree = etree.fromstring(xml)
for serial in tree.findall("./devices/serial"):
if serial.get("type") == "tcp":
source = serial.find("./source")
if source is not None:
if mode and source.get("mode") != mode:
continue
yield (source.get("host"), int(source.get("service")))
@staticmethod
def _get_rbd_driver():
return rbd_utils.RBDDriver(
@ -3637,14 +3661,29 @@ class LibvirtDriver(driver.ComputeDriver):
if ((CONF.libvirt.virt_type == "qemu" or
CONF.libvirt.virt_type == "kvm")):
# The QEMU 'pty' driver throws away any data if no
# client app is connected. Thus we can't get away
# with a single type=pty console. Instead we have
# to configure two separate consoles.
consolelog = vconfig.LibvirtConfigGuestSerial()
consolelog.type = "file"
consolelog.source_path = self._get_console_log_path(instance)
guest.add_device(consolelog)
# Create the serial console char devices
if CONF.serial_console.enabled:
num_ports = hardware.get_number_of_serial_ports(
flavor, image_meta)
for port in six.moves.range(num_ports):
console = vconfig.LibvirtConfigGuestSerial()
console.port = port
console.type = "tcp"
console.listen_host = (
CONF.serial_console.proxyclient_address)
console.listen_port = (
serial_console.acquire_port(
console.listen_host))
guest.add_device(console)
else:
# The QEMU 'pty' driver throws away any data if no
# client app is connected. Thus we can't get away
# with a single type=pty console. Instead we have
# to configure two separate consoles.
consolelog = vconfig.LibvirtConfigGuestSerial()
consolelog.type = "file"
consolelog.source_path = self._get_console_log_path(instance)
guest.add_device(consolelog)
consolepty = vconfig.LibvirtConfigGuestSerial()
else: