Merge "Add VNC console support for the Ironic driver"
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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``
|
||||
Reference in New Issue
Block a user