diff --git a/doc/source/admin/configuration/hypervisor-ironic.rst b/doc/source/admin/configuration/hypervisor-ironic.rst index 4edc61d7c4..9e87d5cd4a 100644 --- a/doc/source/admin/configuration/hypervisor-ironic.rst +++ b/doc/source/admin/configuration/hypervisor-ironic.rst @@ -24,6 +24,13 @@ determine the root partition size when a partition image is used (see the :ironic-doc:`image documentation `). +VNC console support +------------------- + +The nova noVNC VNC console service can connect to bare metal nodes when +ironic has +:ironic-doc:`Graphical console support` +enabled and configured. Configuration ------------- diff --git a/doc/source/admin/remote-console-access.rst b/doc/source/admin/remote-console-access.rst index 9fdd754e01..aacd4b69d0 100644 --- a/doc/source/admin/remote-console-access.rst +++ b/doc/source/admin/remote-console-access.rst @@ -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 diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index ec9fac4021..8b5e07b2b4 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -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 diff --git a/nova/conf/ironic.py b/nova/conf/ironic.py index 9c1087eef7..bd51cda545 100644 --- a/nova/conf/ironic.py +++ b/nova/conf/ironic.py @@ -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', diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py index bf4f861336..49b0a46964 100644 --- a/nova/tests/unit/virt/ironic/test_driver.py +++ b/nova/tests/unit/virt/ironic/test_driver.py @@ -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} diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py index 16c7724da0..7fac064535 100644 --- a/nova/tests/unit/virt/ironic/utils.py +++ b/nova/tests/unit/virt/ironic/utils.py @@ -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'), diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index 6ad9b43d85..292d9965b0 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -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. diff --git a/releasenotes/notes/ironic_vnc_console-bce48b742cb7e520.yaml b/releasenotes/notes/ironic_vnc_console-bce48b742cb7e520.yaml new file mode 100644 index 0000000000..053a112637 --- /dev/null +++ b/releasenotes/notes/ironic_vnc_console-bce48b742cb7e520.yaml @@ -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``