Add VNC console support for the Ironic driver

Ironic is adding support for VNC consoles tracked under the following
spec[1]. This change provides support for the Nova Ironic driver to
access the consoles created by this feature effort.

This supersedes an existing Nova spec[2] to add VNC console support to
the Ironic driver, so this change can be considered to implement this
spec also. This change can be merged independently of the Ironic work,
as the Ironic driver handles the VNC console not being available.

The pre-requesites for a graphical console being available for an Ironic
driver 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``

The associated depends on adds the novnc validation check to the
baremetal basic ops, which is run in job
ironic-tempest-ipa-wholedisk-bios-agent_ipmitool-tinyipa.

In the support matrix console.vnc support is set to partial for ironic
due to the current lack of vencrypt support on the ironic side.

[1] https://specs.openstack.org/openstack/ironic-specs/specs/approved/graphical-console.html
[2] https://specs.openstack.org/openstack/nova-specs/specs/2023.1/approved/ironic-vnc-console.html

Related-Bug: 2086715
Implements: blueprint ironic-vnc-console
Change-Id: Iec26c67e29f91954eafc6a5a81086e36798d3f26
Signed-off-by: Steve Baker <sbaker@redhat.com>
This commit is contained in:
Steve Baker
2025-02-23 21:43:40 +00:00
parent d840c63a18
commit 791310ae1e
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``