diff --git a/doc/source/admin/remote-console-access.rst b/doc/source/admin/remote-console-access.rst index 85ba556cfd47..31f098fea2c0 100644 --- a/doc/source/admin/remote-console-access.rst +++ b/doc/source/admin/remote-console-access.rst @@ -318,7 +318,6 @@ be told where to find them. This requires editing :file:`nova.conf` to set. vencrypt_client_cert=/etc/pki/nova-novncproxy/client-cert.pem vencrypt_ca_certs=/etc/pki/nova-novncproxy/ca-cert.pem - .. _spice-console: SPICE console @@ -403,6 +402,10 @@ for SPICE consoles. - :oslo.config:option:`spice.playback_compression` - :oslo.config:option:`spice.streaming_mode` +As well as the following option to require that connections be protected by TLS: + +- :oslo.config:option:`spice.require_secure` + .. _serial-console: Serial console diff --git a/nova/conf/spice.py b/nova/conf/spice.py index e5854946f15d..d01a83c3b937 100644 --- a/nova/conf/spice.py +++ b/nova/conf/spice.py @@ -194,6 +194,23 @@ Related options: * This option depends on the ``server_listen`` option. The proxy client must be able to access the address specified in ``server_listen`` using the value of this option. +"""), + cfg.BoolOpt('require_secure', + default=False, + help=""" +Whether to require secure TLS connections to SPICE consoles. + +If you're providing direct access to SPICE consoles instead of using the HTML5 +proxy, you may wish those connections to be encrypted. If so, set this value to +True. + +Note that use of secure consoles requires that you setup TLS certificates on +each hypervisor. + +Possible values: + +* False: console traffic is not encrypted. +* True: console traffic is required to be protected by TLS. """), ] diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 01c5e434854a..013307f0cdf1 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -1715,9 +1715,9 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): obj.listen = "127.0.0.1" xml = obj.to_xml() - self.assertXmlEqual(xml, """ + self.assertXmlEqual(""" - """) + """, xml) def test_config_graphics_spice(self): obj = config.LibvirtConfigGuestGraphics() @@ -1726,6 +1726,18 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): obj.keymap = "en_US" obj.listen = "127.0.0.1" + xml = obj.to_xml() + self.assertXmlEqual(""" + + """, xml) + + def test_config_graphics_spice_compression(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + obj.image_compression = "auto_glz" obj.jpeg_compression = "auto" obj.zlib_compression = "always" @@ -1733,7 +1745,7 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): obj.streaming_mode = "filter" xml = obj.to_xml() - self.assertXmlEqual(xml, """ + self.assertXmlEqual(""" @@ -1741,7 +1753,64 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): - """) + """, xml) + + def test_config_graphics_spice_secure(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + + obj.secure = True + + xml = obj.to_xml() + self.assertXmlEqual(""" + + + + + + + + + + + """, xml) + + def test_config_graphics_spice_secure_compression(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + + obj.secure = True + + obj.image_compression = "auto_glz" + obj.jpeg_compression = "auto" + obj.zlib_compression = "always" + obj.playback_compression = True + obj.streaming_mode = "filter" + + xml = obj.to_xml() + self.assertXmlEqual(""" + + + + + + + + + + + + + + + + """, xml) class LibvirtConfigGuestHostdev(LibvirtConfigBaseTest): diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 8046a4f7efda..ae37a57b9221 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -5897,6 +5897,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[3].zlib_compression) self.assertIsNone(cfg.devices[3].playback_compression) self.assertIsNone(cfg.devices[3].streaming_mode) + self.assertFalse(cfg.devices[3].secure) def test_get_guest_config_with_vnc_and_tablet(self): self.flags(enabled=True, group='vnc') @@ -5932,6 +5933,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[3].zlib_compression) self.assertIsNone(cfg.devices[3].playback_compression) self.assertIsNone(cfg.devices[3].streaming_mode) + self.assertFalse(cfg.devices[3].secure) self.assertEqual(cfg.devices[5].type, 'tablet') def test_get_guest_config_with_spice_and_tablet(self): @@ -5973,6 +5975,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[3].zlib_compression) self.assertIsNone(cfg.devices[3].playback_compression) self.assertIsNone(cfg.devices[3].streaming_mode) + self.assertFalse(cfg.devices[3].secure) self.assertEqual(cfg.devices[5].type, 'tablet') @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) @@ -6037,6 +6040,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[4].zlib_compression) self.assertIsNone(cfg.devices[4].playback_compression) self.assertIsNone(cfg.devices[4].streaming_mode) + self.assertFalse(cfg.devices[4].secure) self.assertEqual(cfg.devices[5].type, video_type) def test_get_guest_config_with_spice_compression(self): @@ -6082,6 +6086,43 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(cfg.devices[3].zlib_compression, 'always') self.assertFalse(cfg.devices[3].playback_compression) self.assertEqual(cfg.devices[3].streaming_mode, 'all') + self.assertFalse(cfg.devices[3].secure) + + def test_get_guest_config_with_spice_secure(self): + self.flags(enabled=False, group='vnc') + self.flags(virt_type='kvm', group='libvirt') + self.flags(enabled=True, + agent_enabled=False, + require_secure=True, + server_listen='10.0.0.1', + group='spice') + self.flags(pointer_model='usbtablet') + + cfg = self._get_guest_config_with_graphics() + + self.assertEqual(len(cfg.devices), 9) + 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.LibvirtConfigGuestGraphics) + self.assertIsInstance(cfg.devices[4], + vconfig.LibvirtConfigGuestVideo) + self.assertIsInstance(cfg.devices[5], + vconfig.LibvirtConfigGuestInput) + self.assertIsInstance(cfg.devices[6], + vconfig.LibvirtConfigGuestRng) + self.assertIsInstance(cfg.devices[7], + vconfig.LibvirtConfigGuestUSBHostController) + self.assertIsInstance(cfg.devices[8], + vconfig.LibvirtConfigMemoryBalloon) + + self.assertEqual(cfg.devices[3].type, 'spice') + self.assertEqual(cfg.devices[3].listen, '10.0.0.1') + self.assertTrue(cfg.devices[3].secure) @mock.patch.object(host.Host, 'get_guest') @mock.patch.object(libvirt_driver.LibvirtDriver, diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index a4395b4d28ee..059b3c686ddc 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -2188,6 +2188,7 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): self.autoport = True self.keymap = None self.listen = None + self.secure = None self.image_compression = None self.jpeg_compression = None @@ -2222,6 +2223,11 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): if self.streaming_mode is not None: dev.append(etree.Element( 'streaming', mode=self.streaming_mode)) + if self.secure: + for channel in ['main', 'display', 'inputs', 'cursor', + 'playback', 'record', 'smartcard', 'usbredir']: + dev.append(etree.Element('channel', name=channel, + mode='secure')) return dev diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index e90c6284544e..7e701c740d44 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -7692,6 +7692,7 @@ class LibvirtDriver(driver.ComputeDriver): graphics.zlib_compression = CONF.spice.zlib_compression graphics.playback_compression = CONF.spice.playback_compression graphics.streaming_mode = CONF.spice.streaming_mode + graphics.secure = CONF.spice.require_secure guest.add_device(graphics) add_video_driver = True diff --git a/releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml b/releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml new file mode 100644 index 000000000000..0149fa051d21 --- /dev/null +++ b/releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + This release adds a new config option require_secure to the spice + configuration group. Defaulting to false to match the previous + behavior, if set to true the SPICE consoles will require TLS + protected connections. Unencrypted connections will be gracefully + redirected to the TLS port via the SPICE protocol.