Merge "Add VNC console support for the Ironic driver"

This commit is contained in:
Zuul
2026-02-23 19:04:09 +00:00
committed by Gerrit Code Review
8 changed files with 282 additions and 1 deletions
@@ -24,6 +24,13 @@ determine the root partition size when a partition image is used (see the
:ironic-doc:`image documentation
<install/configure-glance-images.html>`).
VNC console support
-------------------
The nova noVNC VNC console service can connect to bare metal nodes when
ironic has
:ironic-doc:`Graphical console support<install/graphical-console.html>`
enabled and configured.
Configuration
-------------
@@ -184,6 +184,12 @@ Replace ``IP_ADDRESS`` with the IP address from which the proxy is accessible
by the outside world. For example, this may be the management interface IP
address of the controller or the VIP.
If using the ironic hypervisor driver, the following additional option is
supported:
- :oslo.config:option:`ironic.vnc_console_state_timeout`
.. _vnc-security:
VNC proxy security
@@ -271,6 +277,11 @@ set.
[vnc]
auth_schemes=vencrypt,none
.. note::
Enabling ``vencrypt`` is not yet implemented for ironic, so this is not
yet supported for the ironic hypervisor driver.
The :oslo.config:option:`vnc.auth_schemes` values should be listed in order
of preference. If enabling VeNCrypt on an existing deployment which already has
instances running, the noVNC proxy server must initially be allowed to use
+1 -1
View File
@@ -1017,7 +1017,7 @@ driver.libvirt-kvm-s390x=missing
driver.libvirt-qemu-x86=complete
driver.libvirt-lxc=missing
driver.vmware=complete
driver.ironic=missing
driver.ironic=partial
driver.libvirt-vz-vm=complete
driver.libvirt-vz-ct=complete
driver.zvm=missing
+7
View File
@@ -68,6 +68,13 @@ Related options:
min=0,
help='Timeout (seconds) to wait for node serial console state '
'changed. Set to 0 to disable timeout.'),
cfg.IntOpt(
'vnc_console_state_timeout',
default=60,
min=0,
help='Timeout (seconds) to wait for node VNC console state '
'changed. Set to 0 to disable timeout (not recommended in '
'production environments).'),
cfg.StrOpt(
'conductor_group',
deprecated_name='partition_key',
+135
View File
@@ -3444,6 +3444,8 @@ class IronicDriverConsoleTestCase(test.NoDBTestCase):
CONF.set_default('api_retry_interval', default=0, group='ironic')
CONF.set_default(
'serial_console_state_timeout', default=1, group='ironic')
CONF.set_default(
'vnc_console_state_timeout', default=1, group='ironic')
self.stub_out('nova.virt.ironic.driver.IronicDriver.'
'_validate_instance_and_node',
@@ -3591,6 +3593,139 @@ class IronicDriverConsoleTestCase(test.NoDBTestCase):
mock_timer.start.assert_called_with(starting_interval=0.05, timeout=1,
jitter=0.5)
@mock.patch.object(ironic_driver, '_CONSOLE_STATE_CHECKING_INTERVAL', 0.05)
def test_get_vnc_console_disabled(self):
console_data_disabled = self._create_console_data(
enabled=False,
console_type='vnc',
url='http://127.0.0.1:10001/vnc_lite.html')
console_data_enabled = self._create_console_data(
enabled=True,
console_type='vnc',
url='http://127.0.0.1:10001/vnc_lite.html')
self.mock_conn.get_node_console.side_effect = [
console_data_disabled,
console_data_disabled,
console_data_disabled,
console_data_enabled
]
self.mock_conn.get_node.return_value = _get_cached_node(
driver_internal_info={
'vnc_host': '127.0.0.1',
'vnc_port': 10000})
result = self.driver.get_vnc_console(self.ctx, self.instance)
self.assertEqual(4, self.mock_conn.get_node_console.call_count)
self.assertEqual(1, self.mock_conn.enable_node_console.call_count)
self.assertIsInstance(result, console_type.ConsoleVNC)
self.assertEqual('127.0.0.1', result.host)
self.assertEqual(10000, result.port)
@mock.patch.object(ironic_driver, '_CONSOLE_STATE_CHECKING_INTERVAL', 0.05)
def test_get_vnc_console_enable_timeout(self):
self.mock_conn.get_node_console.return_value = \
self._create_console_data(
enabled=False,
console_type='vnc',
url='http://127.0.0.1:10001/vnc_lite.html')
self.mock_conn.get_node.return_value = _get_cached_node(
driver_internal_info={
'vnc_host': '127.0.0.1',
'vnc_port': 10000})
self.assertRaises(exception.ConsoleTypeUnavailable,
self.driver.get_vnc_console,
self.ctx, self.instance)
self.assertGreater(self.mock_conn.get_node_console.call_count, 2)
self.assertEqual(1, self.mock_conn.enable_node_console.call_count)
def test_get_vnc_console_enabled(self):
self.mock_conn.get_node_console.return_value = \
self._create_console_data(
enabled=True,
console_type='vnc',
url='http://127.0.0.1:10001/vnc_lite.html')
self.mock_conn.get_node.return_value = _get_cached_node(
driver_internal_info={
'vnc_host': '127.0.0.1',
'vnc_port': 10000})
result = self.driver.get_vnc_console(self.ctx, self.instance)
self.assertEqual(1, self.mock_conn.get_node_console.call_count)
self.assertEqual(0, self.mock_conn.enable_node_console.call_count)
self.assertIsInstance(result, console_type.ConsoleVNC)
self.assertEqual('127.0.0.1', result.host)
self.assertEqual(10000, result.port)
def test_get_vnc_console_missing_internal_info(self):
self.mock_conn.get_node_console.return_value = \
self._create_console_data(
enabled=True,
console_type='vnc',
url='http://127.0.0.1:10001/vnc_lite.html')
self.mock_conn.get_node.return_value = _get_cached_node(
driver_internal_info={})
self.assertRaises(exception.ConsoleTypeUnavailable,
self.driver.get_vnc_console,
self.ctx, self.instance)
self.assertEqual(1, self.mock_conn.get_node_console.call_count)
self.assertEqual(0, self.mock_conn.enable_node_console.call_count)
def test_get_vnc_console_wrong_type(self):
self.mock_conn.get_node_console.return_value = \
self._create_console_data(
enabled=True,
console_type='socat',
url='tcp://127.0.0.1:10001')
self.assertRaises(exception.ConsoleTypeUnavailable,
self.driver.get_vnc_console,
self.ctx, self.instance)
self.assertEqual(1, self.mock_conn.get_node_console.call_count)
self.assertEqual(0, self.mock_conn.enable_node_console.call_count)
def test_get_vnc_console_api_error(self):
self.mock_conn.get_node_console.side_effect = \
sdk_exc.ForbiddenException()
self.assertRaises(exception.ConsoleTypeUnavailable,
self.driver.get_vnc_console,
self.ctx, self.instance)
self.assertEqual(1, self.mock_conn.get_node_console.call_count)
def test_get_vnc_console_enable_api_error(self):
self.mock_conn.get_node_console.return_value = \
self._create_console_data(
enabled=False,
console_type='vnc',
url='http://127.0.0.1:10001/vnc_lite.html')
self.mock_conn.enable_node_console.side_effect = \
sdk_exc.ForbiddenException()
self.assertRaises(exception.ConsoleTypeUnavailable,
self.driver.get_vnc_console,
self.ctx, self.instance)
self.assertEqual(1, self.mock_conn.get_node_console.call_count)
def test_get_serial_console_socat(self):
temp_data = {'target_mode': True}
+1
View File
@@ -73,6 +73,7 @@ def get_test_node(fields=None, **kw):
'instance_info': kw.get('instance_info'),
'driver': kw.get('driver', 'fake'),
'driver_info': kw.get('driver_info', {}),
'driver_internal_info': kw.get('driver_internal_info', {}),
'properties': kw.get('properties', {}),
'reservation': kw.get('reservation'),
'is_maintenance': kw.get('is_maintenance'),
+109
View File
@@ -1926,6 +1926,115 @@ class IronicDriver(virt_driver.ComputeDriver):
instance.uuid)
raise exception.ConsoleNotAvailable()
def _get_vnc_console(self, instance):
"""Acquire novnc console information for an instance.
:param instance: nova instance
:return: a dictionary with below values
{ 'node': ironic node
'console_info': node console info }
:raise ConsoleNotAvailable: if console is unavailable
for the instance
"""
node = self._validate_instance_and_node(instance)
node_id = node.id
def _get_console():
"""Request to acquire node console."""
try:
return self.ironic_connection.get_node_console(node_id)
except sdk_exc.SDKException as e:
LOG.error('Failed to acquire console information for '
'instance %(inst)s: %(reason)s',
{'inst': instance.uuid, 'reason': e})
raise exception.ConsoleNotAvailable()
def _wait_state():
"""Wait for the console to be enabled"""
console = _get_console()
if console['console_enabled']:
raise loopingcall.LoopingCallDone(retvalue=console)
_log_ironic_polling('set vnc console mode', node, instance)
# Return False to start backing off
return False
def _enable_console():
"""Request to enable/disable node console."""
try:
self.ironic_connection.enable_node_console(node_id)
except sdk_exc.SDKException as e:
LOG.error('Failed to set console mode to "True" '
'for instance %(inst)s: %(reason)s',
{'inst': instance.uuid,
'reason': e})
raise exception.ConsoleNotAvailable()
# Waiting for the console state to be enabled
try:
timer = loopingcall.BackOffLoopingCall(_wait_state)
return timer.start(
starting_interval=_CONSOLE_STATE_CHECKING_INTERVAL,
timeout=CONF.ironic.vnc_console_state_timeout,
jitter=0.5).wait()
except loopingcall.LoopingCallTimeOut:
LOG.error('Timeout while waiting for console_enabled to be '
'set to "True" on node %(node)s',
{'node': node_id})
raise exception.ConsoleNotAvailable()
# Acquire the console
console = _get_console()
if not console['console_enabled']:
console = _enable_console()
return {'node': node,
'console_info': console['console_info']}
def get_vnc_console(self, context, instance):
"""Acquire VNC console information.
:param context: request context
:param instance: nova instance
:return: ConsoleSerial object
:raise ConsoleTypeUnavailable: if VNC console is unavailable
for the instance
"""
LOG.debug('Getting VNC console', instance=instance)
try:
result = self._get_vnc_console(instance)
except exception.ConsoleNotAvailable:
raise exception.ConsoleTypeUnavailable(console_type='vnc')
console_info = result['console_info']
if console_info["type"] != "vnc":
LOG.warning('Console type "%(type)s" (of ironic node '
'%(node)s) does not support Nova VNC console',
{'type': console_info["type"],
'node': instance.node},
instance=instance)
raise exception.ConsoleTypeUnavailable(console_type='vnc')
# NOTE(stevebaker): The URL provided in the console_info is actually a
# NoVNC URL from Ironic's own novncproxy, so get the actual VNC host
# and port from the node driver_internal_info.
node = self.ironic_connection.get_node(
instance.node, fields=('uuid', 'driver_internal_info'))
host = node.driver_internal_info.get('vnc_host')
port = node.driver_internal_info.get('vnc_port')
if host is None or port is None:
LOG.error('Invalid VNC console URL "%(url)s" '
'(ironic node %(node)s)',
{'url': console_info["url"],
'node': node.id},
instance=instance)
raise exception.ConsoleTypeUnavailable(console_type='vnc')
return console_type.ConsoleVNC(host=host, port=port)
def get_serial_console(self, context, instance):
"""Acquire serial console information.
@@ -0,0 +1,11 @@
---
features:
- |
The Ironic driver now supports VNC consoles when available. The
pre-requesites for this being available for a node is:
- Ironic is configured to enable graphical consoles
- The node ``console_interface`` is a graphical driver such as
``redfish-graphical`` or ``fake-graphical``
- ``nova-novncproxy`` can make network connections to the VNC servers
which run adjacent to ``ironic-conductor``