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.