diff --git a/doc/source/admin/remote-console-access.rst b/doc/source/admin/remote-console-access.rst index 85ba556cfd..31f098fea2 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 e5854946f1..d01a83c3b9 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 01c5e43485..013307f0cd 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 44468584b3..dc35d4044c 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -5907,6 +5907,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') @@ -5942,6 +5943,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): @@ -5983,6 +5985,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()) @@ -6047,6 +6050,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): @@ -6092,6 +6096,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 a4395b4d28..059b3c686d 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 4688d6848d..a95017c3f5 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -7711,6 +7711,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 0000000000..0149fa051d --- /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.