From 41628d3546dee32f36033840cb22d5c157d40dc4 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 13 May 2022 14:45:28 +1200 Subject: [PATCH] Add SecureBoot support to the emulator, all drivers This change adds a basic SecureBoot endpoint to the emulator which returns the actual secure boot enabled status. The fake and libvirt drivers supports setting the secure boot. OVMF paths have aligned on /usr/share/OVMF/ since this is supported by Red Hat and Debian family distros. Default and example firmware loader is now OVMF_CODE.secboot.fd since this exists on all distros and is known to work with both secure boot enabled and disabled on some distros. Change-Id: Icb85710d81c656da6853d67f5e1c6a8609abc1cf --- doc/source/admin/emulator.conf | 19 +- doc/source/user/dynamic-emulator.rst | 39 ++-- .../notes/secure-boot-de663109ced9b266.yaml | 12 ++ sushy_tools/emulator/main.py | 27 +++ .../emulator/resources/systems/base.py | 18 ++ .../emulator/resources/systems/fakedriver.py | 6 + .../resources/systems/libvirtdriver.py | 137 +++++++++---- .../emulator/resources/systems/novadriver.py | 28 +++ .../emulator/templates/secure_boot.json | 12 ++ sushy_tools/emulator/templates/system.json | 3 + sushy_tools/error.py | 3 + .../tests/unit/emulator/domain-q35_uefi.xml | 29 +++ .../unit/emulator/domain-q35_uefi_secure.xml | 29 +++ .../resources/systems/test_fakedriver.py | 5 + .../resources/systems/test_libvirt.py | 180 ++++++++++++------ .../emulator/resources/systems/test_nova.py | 25 +++ sushy_tools/tests/unit/emulator/test_main.py | 34 ++++ 17 files changed, 500 insertions(+), 106 deletions(-) create mode 100644 releasenotes/notes/secure-boot-de663109ced9b266.yaml create mode 100644 sushy_tools/emulator/templates/secure_boot.json create mode 100644 sushy_tools/tests/unit/emulator/domain-q35_uefi.xml create mode 100644 sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml diff --git a/doc/source/admin/emulator.conf b/doc/source/admin/emulator.conf index 4dee4cf2..5236ff45 100644 --- a/doc/source/admin/emulator.conf +++ b/doc/source/admin/emulator.conf @@ -32,18 +32,23 @@ SUSHY_EMULATOR_IGNORE_BOOT_DEVICE = False # The map of firmware loaders dependant on the boot mode and -# system architecture +# system architecture. Ideally the x86_64 loader will be capable +# of secure boot or not based on the chosen nvram. SUSHY_EMULATOR_BOOT_LOADER_MAP = { - u'UEFI': { - u'x86_64': u'/usr/share/OVMF/OVMF_CODE.fd', - u'aarch64': u'/usr/share/AAVMF/AAVMF_CODE.fd' + 'UEFI': { + 'x86_64': u'/usr/share/OVMF/OVMF_CODE.secboot.fd', + 'aarch64': u'/usr/share/AAVMF/AAVMF_CODE.fd' }, - u'Legacy': { - u'x86_64': None, - u'aarch64': None + 'Legacy': { + 'x86_64': None, + 'aarch64': None } } +# nvram templates to use on x86_64 to enable or disable secure boot +SUSHY_EMULATOR_SECURE_BOOT_ENABLED_NVRAM = '/usr/share/OVMF/OVMF_VARS.secboot.fd' +SUSHY_EMULATOR_SECURE_BOOT_DISABLED_NVRAM = '/usr/share/OVMF/OVMF_VARS.fd' + # This map contains statically configured Redfish Chassis linked # up with the Systems and Managers enclosed into this Chassis. # diff --git a/doc/source/user/dynamic-emulator.rst b/doc/source/user/dynamic-emulator.rst index 9ff23be0..faee7af6 100644 --- a/doc/source/user/dynamic-emulator.rst +++ b/doc/source/user/dynamic-emulator.rst @@ -152,8 +152,8 @@ On the host you need to have OVMF firmware binaries installed. Fedora users could pull them as `edk2-ovmf` RPM. On Ubuntu, `apt-get install ovmf` should do the job. -Then you need to create a VM by running `virt-install` with the `--boot uefi` -option: +Then you need to create a VM by running `virt-install` with the UEFI specific +`--boot` options: Example: @@ -163,7 +163,11 @@ Example: virt-install \ --name vbmc-node \ --ram 1024 \ - --boot uefi \ + --boot loader.readonly=yes \ + --boot loader.type=pflash \ + --boot loader.secure=no \ + --boot loader=/usr/share/OVMF/OVMF_CODE.secboot.fd \ + --boot nvram.template=/usr/share/OVMF/OVMF_VARS.fd \ --disk size=1 \ --vcpus 2 \ --os-type linux \ @@ -174,16 +178,27 @@ Example: rm $tmpfile This will create a new `libvirt` domain with path to OVMF images properly -configured. Let's take a note on the path to the blob: +configured. Let's take a note on the path to the blob by running `virsh dumpxml vbmc-node`: -.. code-block:: bash +Example: - $ virsh dumpxml vbmc-node | grep loader - /usr/share/edk2/ovmf/OVMF_CODE.fd +.. code-block:: xml -Because now we need to add this path to emulator's configuration matching -VM architecture we are running. Make a copy of stock configuration file -and edit it accordingly: + + ... + + hvm + /usr/share/edk2/ovmf/OVMF_CODE.secboot.fd + + + + ... + + +Because now we need to add this path to emulator's configuration matching VM +architecture we are running. It is also possible to make Redfish calls to enable +or disable Secure Boot by specifying which nvram template to load in each case. +Make a copy of stock configuration file and edit it accordingly: .. code-block:: bash @@ -191,9 +206,11 @@ and edit it accordingly: ... SUSHY_EMULATOR_BOOT_LOADER_MAP = { 'Uefi': { - 'x86_64': '/usr/share/edk2/ovmf/OVMF_CODE.fd', + 'x86_64': '/usr/share/OVMF/OVMF_CODE.secboot.fd', ... } + SUSHY_EMULATOR_SECURE_BOOT_ENABLED_NVRAM = '/usr/share/OVMF/OVMF_VARS.secboot.fd' + SUSHY_EMULATOR_SECURE_BOOT_DISABLED_NVRAM = '/usr/share/OVMF/OVMF_VARS.fd' ... Now you can run `sushy-emulator` with the updated configuration file: diff --git a/releasenotes/notes/secure-boot-de663109ced9b266.yaml b/releasenotes/notes/secure-boot-de663109ced9b266.yaml new file mode 100644 index 00000000..609ddf0d --- /dev/null +++ b/releasenotes/notes/secure-boot-de663109ced9b266.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + It is now possible to enable and disable UEFI Secure Boot mode via Redfish + requests for the libvirt driver. This is possible by configuring domains to + use a secure boot capable firmware loader, and setting configuration values + `SUSHY_EMULATOR_SECURE_BOOT_ENABLED_NVRAM` and + `SUSHY_EMULATOR_SECURE_BOOT_DISABLED_NVRAM` to nvram template paths which + enable or disable secure boot. + + The fake driver supports getting and setting secure boot, the nova driver + only supports getting. \ No newline at end of file diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index dae6bf15..192fb9ad 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -558,6 +558,33 @@ def system_reset_bios(identity): return '', 204 +@app.route('/redfish/v1/Systems//SecureBoot', + methods=['GET', 'PATCH']) +@api_utils.ensure_instance_access +@api_utils.returns_json +def secure_boot(identity): + + if flask.request.method == 'GET': + secure = app.systems.get_secure_boot(identity) + + app.logger.debug('Serving secure boot for system "%s"', identity) + + return flask.render_template( + 'secure_boot.json', + identity=identity, + secure_boot_enable=secure, + secure_boot_current_boot=secure and 'Enabled' or 'Disabled') + + elif flask.request.method == 'PATCH': + secure = flask.request.json.get('SecureBootEnable') + + app.systems.set_secure_boot(identity, secure) + + app.logger.info('System "%s" secure boot updated to "%s"', + identity, secure) + return '', 204 + + @app.route('/redfish/v1/Systems//SimpleStorage', methods=['GET']) @api_utils.ensure_instance_access diff --git a/sushy_tools/emulator/resources/systems/base.py b/sushy_tools/emulator/resources/systems/base.py index 67a7ab20..440e8cd6 100644 --- a/sushy_tools/emulator/resources/systems/base.py +++ b/sushy_tools/emulator/resources/systems/base.py @@ -124,6 +124,24 @@ class AbstractSystemsDriver(metaclass=abc.ABCMeta): """ raise error.NotSupportedError('Not implemented') + def get_secure_boot(self, identity): + """Get computer system secure boot state for UEFI boot mode. + + :returns: boolean of the current secure boot state + + :raises: `FishyError` if the state can't be fetched + """ + raise error.NotSupportedError('Not implemented') + + def set_secure_boot(self, identity, secure): + """Set computer system secure boot state for UEFI boot mode. + + :param secure: boolean requesting the secure boot state + + :raises: `FishyError` if the can't be set + """ + raise error.NotSupportedError('Not implemented') + def get_total_memory(self, identity): """Get computer system total memory diff --git a/sushy_tools/emulator/resources/systems/fakedriver.py b/sushy_tools/emulator/resources/systems/fakedriver.py index 22448b87..1fe03dd3 100644 --- a/sushy_tools/emulator/resources/systems/fakedriver.py +++ b/sushy_tools/emulator/resources/systems/fakedriver.py @@ -143,6 +143,12 @@ class FakeDriver(AbstractSystemsDriver): def set_boot_mode(self, identity, boot_mode): self._update(identity, boot_mode=boot_mode) + def get_secure_boot(self, identity): + return self._get(identity).get('secure_boot', False) + + def set_secure_boot(self, identity, secure): + self._update(identity, secure_boot=secure) + def get_boot_image(self, identity, device): devinfo = self._get(identity).get('boot_image') or {} return devinfo.get(device) or (None, False, False) diff --git a/sushy_tools/emulator/resources/systems/libvirtdriver.py b/sushy_tools/emulator/resources/systems/libvirtdriver.py index 4168ee33..18160310 100644 --- a/sushy_tools/emulator/resources/systems/libvirtdriver.py +++ b/sushy_tools/emulator/resources/systems/libvirtdriver.py @@ -101,7 +101,7 @@ class LibvirtDriver(AbstractSystemsDriver): BOOT_LOADER_MAP = { 'UEFI': { - 'x86_64': '/usr/share/OVMF/OVMF_CODE.fd', + 'x86_64': '/usr/share/OVMF/OVMF_CODE.secboot.fd', 'aarch64': '/usr/share/AAVMF/AAVMF_CODE.fd' }, 'Legacy': { @@ -111,6 +111,9 @@ class LibvirtDriver(AbstractSystemsDriver): } + SECURE_BOOT_ENABLED_NVRAM = '/usr/share/OVMF/OVMF_VARS.secboot.fd' + SECURE_BOOT_DISABLED_NVRAM = '/usr/share/OVMF/OVMF_VARS.fd' + DEVICE_TYPE_MAP = { constants.DEVICE_TYPE_CD: 'cdrom', constants.DEVICE_TYPE_FLOPPY: 'floppy', @@ -161,6 +164,12 @@ class LibvirtDriver(AbstractSystemsDriver): 'SUSHY_EMULATOR_BOOT_LOADER_MAP', cls.BOOT_LOADER_MAP) cls.KNOWN_BOOT_LOADERS = set(y for x in cls.BOOT_LOADER_MAP.values() for y in x.values()) + cls.SECURE_BOOT_ENABLED_NVRAM = cls._config.get( + 'SUSHY_EMULATOR_SECURE_BOOT_ENABLED_NVRAM', + cls.SECURE_BOOT_ENABLED_NVRAM) + cls.SECURE_BOOT_DISABLED_NVRAM = cls._config.get( + 'SUSHY_EMULATOR_SECURE_BOOT_DISABLED_NVRAM', + cls.SECURE_BOOT_DISABLED_NVRAM) cls.SUSHY_EMULATOR_IGNORE_BOOT_DEVICE = \ cls._config.get('SUSHY_EMULATOR_IGNORE_BOOT_DEVICE', False) return cls @@ -503,11 +512,34 @@ class LibvirtDriver(AbstractSystemsDriver): :raises: `error.FishyError` if boot mode can't be set """ + domain = self._get_domain(identity, readonly=True) - # XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS + # XML schema: + # https://libvirt.org/formatdomain.html#operating-system-booting tree = ET.fromstring(self.get_xml_desc(domain)) + self._build_os_element(identity, tree, boot_mode) + with libvirt_open(self._uri) as conn: + + try: + conn.defineXML(ET.tostring(tree).decode('utf-8')) + + except libvirt.libvirtError as e: + msg = ('Error changing boot mode at libvirt URI ' + '"%(uri)s": %(error)s' % {'uri': self._uri, + 'error': e}) + + raise error.FishyError(msg) + + def _build_os_element(self, identity, tree, boot_mode, secure=None): + """Set the boot mode and secure boot on the os element + + This also converts from the previous manual layout to the automatic + approach. + + :raises: `error.FishyError` if boot mode can't be set + """ try: loader_type = self.BOOT_MODE_MAP[boot_mode] @@ -529,7 +561,6 @@ class LibvirtDriver(AbstractSystemsDriver): type_element = os_element.find('type') if type_element is None: os_arch = None - else: os_arch = type_element.get('arch') @@ -544,41 +575,83 @@ class LibvirtDriver(AbstractSystemsDriver): boot_mode, os_arch) loader_path = None - loader_elements = os_element.findall('loader') - if len(loader_elements) > 1: - msg = ('Can\'t set boot mode because "loader" element must be ' - 'present exactly once in domain "%(identity)s" ' + # delete loader and nvram elements to rebuild from stratch + for element in os_element.findall('loader'): + os_element.remove(element) + for element in os_element.findall('nvram'): + os_element.remove(element) + + loader_element = ET.SubElement(os_element, 'loader') + loader_element.set('type', loader_type) + if loader_path: + loader_element.text = loader_path + loader_element.set('readonly', 'yes') + + if boot_mode == 'UEFI': + nvram_element = ET.SubElement(os_element, 'nvram') + if secure: + nvram_suffix = '.secboot.fd' + loader_element.set('secure', 'yes') + nvram_element.set('template', self.SECURE_BOOT_ENABLED_NVRAM) + else: + nvram_suffix = '.fd' + loader_element.set('secure', 'no') + nvram_element.set('template', self.SECURE_BOOT_DISABLED_NVRAM) + + # force a different nvram path for secure vs not. This will ensure + # it gets regenerated from the template when secure boot mode + # changes + nvram_path = "/var/lib/libvirt/nvram-%s%s" % (identity, + nvram_suffix) + nvram_element.text = nvram_path + + def get_secure_boot(self, identity): + """Get computer system secure boot state for UEFI boot mode. + + :returns: boolean of the current secure boot state + + :raises: `FishyError` if the state can't be fetched + """ + if self.get_boot_mode(identity) == 'Legacy': + msg = 'Legacy boot mode does not support secure boot' + raise error.NotSupportedError(msg) + + domain = self._get_domain(identity, readonly=True) + + # XML schema: + # https://libvirt.org/formatdomain.html#operating-system-booting + tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) + + os_element = tree.find('os') + + nvram = os_element.findall('nvram') + if len(nvram) > 1: + msg = ('Can\'t get secure boot state because "nvram" element ' + 'must be present exactly once in domain "%(identity)s" ' 'configuration' % {'identity': identity}) raise error.FishyError(msg) - if loader_elements: - loader_element = loader_elements[0] + if not nvram: + return False + nvram_template = nvram[0].get('template') + return nvram_template == self.SECURE_BOOT_ENABLED_NVRAM - if loader_element.text not in self.KNOWN_BOOT_LOADERS: - msg = ('Unknown boot loader path "%(path)s" in domain ' - '"%(identity)s" configuration encountered while ' - 'setting boot mode "%(mode)s", system architecture ' - '"%(arch)s". Consider adding this loader path to ' - 'emulator config.' % {'identity': identity, - 'mode': boot_mode, - 'arch': os_arch, - 'path': loader_element.text}) - raise error.FishyError(msg) + def set_secure_boot(self, identity, secure): + """Set computer system secure boot state for UEFI boot mode. - if loader_path: - loader_element.set('type', loader_type) - loader_element.set('readonly', 'yes') - loader_element.text = loader_path + :param secure: boolean requesting the secure boot state - else: - # NOTE(etingof): path must be present or element must be absent - os_element.remove(loader_element) + :raises: `FishyError` if the can't be set + """ + if self.get_boot_mode(identity) == 'Legacy': + msg = 'Legacy boot mode does not support secure boot' + raise error.NotSupportedError(msg) - elif loader_path: - loader_element = ET.SubElement(os_element, 'loader') - loader_element.set('type', loader_type) - loader_element.set('readonly', 'yes') - loader_element.text = loader_path + domain = self._get_domain(identity, readonly=True) + + # XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS + tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) + self._build_os_element(identity, tree, 'UEFI', secure) with libvirt_open(self._uri) as conn: @@ -586,7 +659,7 @@ class LibvirtDriver(AbstractSystemsDriver): conn.defineXML(ET.tostring(tree).decode('utf-8')) except libvirt.libvirtError as e: - msg = ('Error changing boot mode at libvirt URI ' + msg = ('Error changing secure boot at libvirt URI ' '"%(uri)s": %(error)s' % {'uri': self._uri, 'error': e}) diff --git a/sushy_tools/emulator/resources/systems/novadriver.py b/sushy_tools/emulator/resources/systems/novadriver.py index e64bd46b..7a6636ca 100644 --- a/sushy_tools/emulator/resources/systems/novadriver.py +++ b/sushy_tools/emulator/resources/systems/novadriver.py @@ -273,6 +273,34 @@ class OpenStackDriver(AbstractSystemsDriver): raise error.NotSupportedError(msg) + def get_secure_boot(self, identity): + """Get computer system secure boot state for UEFI boot mode. + + :returns: boolean of the current secure boot state + + :raises: `FishyError` if the state can't be fetched + """ + if self.get_boot_mode(identity) == 'Legacy': + msg = 'Legacy boot mode does not support secure boot' + raise error.NotSupportedError(msg) + + instance = self._get_instance(identity) + + image = self._get_image_info(instance.image['id']) + + return getattr(image, 'os_secure_boot', None) == 'required' + + def set_secure_boot(self, identity, secure): + """Set computer system secure boot state for UEFI boot mode. + + :param secure: boolean requesting the secure boot state + + :raises: `FishyError` if the can't be set + """ + msg = ('The cloud driver %(driver)s does not support changing secure ' + 'boot mode through Redfish' % {'driver': self.driver}) + raise error.NotSupportedError(msg) + def get_total_memory(self, identity): """Get computer system total memory diff --git a/sushy_tools/emulator/templates/secure_boot.json b/sushy_tools/emulator/templates/secure_boot.json new file mode 100644 index 00000000..d9c636f2 --- /dev/null +++ b/sushy_tools/emulator/templates/secure_boot.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#SecureBoot.v1_1_0.SecureBoot", + "Id": "SecureBoot", + "Name": "UEFI Secure Boot", + "Actions": {}, + "SecureBootEnable": {{ 'true' if secure_boot_enable else 'false' }}, + "SecureBootCurrentBoot": {{ secure_boot_current_boot|string|tojson }}, + "SecureBootMode": "DeployedMode", + "@odata.id": {{ "/redfish/v1/Systems/%s/SecureBoot"|format(identity)|tojson }}, + "@odata.context": "/redfish/v1/$metadata#SecureBoot.SecureBoot", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} diff --git a/sushy_tools/emulator/templates/system.json b/sushy_tools/emulator/templates/system.json index 1e52276c..5237b4c5 100644 --- a/sushy_tools/emulator/templates/system.json +++ b/sushy_tools/emulator/templates/system.json @@ -67,6 +67,9 @@ "EthernetInterfaces": { "@odata.id": {{ "/redfish/v1/Systems/%s/EthernetInterfaces"|format(identity)|tojson }} }, + "SecureBoot": { + "@odata.id": {{ "/redfish/v1/Systems/%s/SecureBoot"|format(identity)|tojson }} + }, "SimpleStorage": { "@odata.id": {{ "/redfish/v1/Systems/%s/SimpleStorage"|format(identity)|tojson }} }, diff --git a/sushy_tools/error.py b/sushy_tools/error.py index ecb1e69e..3b27e724 100644 --- a/sushy_tools/error.py +++ b/sushy_tools/error.py @@ -29,6 +29,9 @@ class AliasAccessError(FishyError): class NotSupportedError(FishyError): """Feature not supported by resource driver""" + def __init__(self, msg='Unsupported'): + super().__init__(msg) + class NotFound(FishyError): """Entity not found.""" diff --git a/sushy_tools/tests/unit/emulator/domain-q35_uefi.xml b/sushy_tools/tests/unit/emulator/domain-q35_uefi.xml new file mode 100644 index 00000000..5b6e417c --- /dev/null +++ b/sushy_tools/tests/unit/emulator/domain-q35_uefi.xml @@ -0,0 +1,29 @@ + + QEmu-fedora-i686 + c7a5fdbd-cdaf-9455-926a-d65c16db1809 + 219200 + 219200 + 2 + + hvm + /usr/share/OVMF/OVMF_CODE.fd + + + + + /usr/bin/qemu-system-x86_64 + + + + + + + + + + + + + + + diff --git a/sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml b/sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml new file mode 100644 index 00000000..962fedff --- /dev/null +++ b/sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml @@ -0,0 +1,29 @@ + + QEmu-fedora-i686 + c7a5fdbd-cdaf-9455-926a-d65c16db1809 + 219200 + 219200 + 2 + + hvm + /usr/share/OVMF/OVMF_CODE.secboot.fd + + + + + /usr/bin/qemu-system-x86_64 + + + + + + + + + + + + + + + diff --git a/sushy_tools/tests/unit/emulator/resources/systems/test_fakedriver.py b/sushy_tools/tests/unit/emulator/resources/systems/test_fakedriver.py index 3f13014e..faf355b6 100644 --- a/sushy_tools/tests/unit/emulator/resources/systems/test_fakedriver.py +++ b/sushy_tools/tests/unit/emulator/resources/systems/test_fakedriver.py @@ -92,3 +92,8 @@ class FakeDriverTestCase(base.BaseTestCase): self.test_driver.get_boot_image(UUID, 'Cd')) self.assertEqual((None, False, False), self.test_driver.get_boot_image(UUID, 'Hdd')) + + def test_secure_boot(self): + self.assertFalse(self.test_driver.get_secure_boot(UUID)) + self.test_driver.set_secure_boot(UUID, True) + self.assertTrue(self.test_driver.get_secure_boot(UUID)) diff --git a/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py b/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py index 6e777964..d63a4bde 100644 --- a/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py +++ b/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py @@ -328,7 +328,7 @@ class LibvirtDriverTestCase(base.BaseTestCase): self.assertIn(expected, conn_mock.defineXML.call_args[0][0]) @mock.patch('libvirt.openReadOnly', autospec=True) - def test_get_boot_mode(self, libvirt_mock): + def test_get_boot_mode_legacy(self, libvirt_mock): with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f: data = f.read() @@ -340,6 +340,20 @@ class LibvirtDriverTestCase(base.BaseTestCase): self.assertEqual('Legacy', boot_mode) + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_get_boot_mode_uefi(self, libvirt_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35_uefi.xml', + 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + boot_mode = self.test_driver.get_boot_mode(self.uuid) + + self.assertEqual('UEFI', boot_mode) + @mock.patch('libvirt.open', autospec=True) @mock.patch('libvirt.openReadOnly', autospec=True) def test_set_boot_mode(self, libvirt_mock, libvirt_rw_mock): @@ -359,8 +373,9 @@ class LibvirtDriverTestCase(base.BaseTestCase): @mock.patch('libvirt.open', autospec=True) @mock.patch('libvirt.openReadOnly', autospec=True) - def test_set_boot_mode_known_loader(self, libvirt_mock, libvirt_rw_mock): - with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f: + def test_set_boot_mode_legacy(self, libvirt_mock, libvirt_rw_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35_uefi.xml', + 'r') as f: data = f.read() conn_mock = libvirt_mock.return_value @@ -369,37 +384,15 @@ class LibvirtDriverTestCase(base.BaseTestCase): with mock.patch.object( self.test_driver, 'get_power_state', return_value='Off'): - self.test_driver.set_boot_mode(self.uuid, 'UEFI') + self.test_driver.set_boot_mode(self.uuid, 'Legacy') conn_mock = libvirt_rw_mock.return_value xml_document = conn_mock.defineXML.call_args[0][0] tree = ET.fromstring(xml_document) os_element = tree.find('os') loader_element = os_element.find('loader') - self.assertEqual( - 'pflash', loader_element.get('type')) - self.assertEqual( - '/usr/share/OVMF/OVMF_CODE.fd', - loader_element.text) - - @mock.patch('libvirt.open', autospec=True) - @mock.patch('libvirt.openReadOnly', autospec=True) - def test_set_boot_mode_unknown_loader_path( - self, libvirt_mock, libvirt_rw_mock): - with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f: - data = f.read() - - conn_mock = libvirt_mock.return_value - domain_mock = conn_mock.lookupByUUID.return_value - domain_mock.XMLDesc.return_value = data - - with mock.patch.dict( - self.test_driver.KNOWN_BOOT_LOADERS, {}, clear=True): - with mock.patch.object( - self.test_driver, 'get_power_state', return_value='Off'): - self.assertRaises( - error.FishyError, self.test_driver.set_boot_mode, - self.uuid, 'Uefi') + self.assertIsNone(loader_element.text) + self.assertNotIn('readonly', loader_element.attrib) @mock.patch('libvirt.open', autospec=True) @mock.patch('libvirt.openReadOnly', autospec=True) @@ -467,34 +460,6 @@ class LibvirtDriverTestCase(base.BaseTestCase): error.FishyError, self.test_driver.set_boot_mode, self.uuid, 'Uefi') - @mock.patch('libvirt.open', autospec=True) - @mock.patch('libvirt.openReadOnly', autospec=True) - def test_set_boot_mode_no_type(self, libvirt_mock, libvirt_rw_mock): - with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f: - data = f.read() - - tree = ET.fromstring(data) - os_element = tree.find('os') - type_element = os_element.find('type') - os_element.remove(type_element) - - data = ET.tostring(tree) - - conn_mock = libvirt_mock.return_value - domain_mock = conn_mock.lookupByUUID.return_value - domain_mock.XMLDesc.return_value = data - - with mock.patch.object( - self.test_driver, 'get_power_state', return_value='Off'): - self.test_driver.set_boot_mode(self.uuid, 'UEFI') - - conn_mock = libvirt_rw_mock.return_value - xml_document = conn_mock.defineXML.call_args[0][0] - tree = ET.fromstring(xml_document) - os_element = tree.find('os') - # NOTE(etingof): should enforce default loader - self.assertIsNone(os_element.find('loader')) - @mock.patch('libvirt.openReadOnly', autospec=True) def test_get_boot_image(self, libvirt_mock): with open('sushy_tools/tests/unit/emulator/domain.xml', 'r') as f: @@ -1117,3 +1082,106 @@ class LibvirtDriverTestCase(base.BaseTestCase): self.test_driver.find_or_create_storage_volume(vol_data) pool_mock.createXML.assert_called_once_with(mock.ANY) + + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_get_secure_boot_off(self, libvirt_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35_uefi.xml', + 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + self.assertFalse(self.test_driver.get_secure_boot(self.uuid)) + + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_get_secure_boot_on(self, libvirt_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml', + 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + self.assertTrue(self.test_driver.get_secure_boot(self.uuid)) + + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_get_secure_boot_not_uefi(self, libvirt_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35.xml', 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + self.assertRaises(error.NotSupportedError, + self.test_driver.get_secure_boot, self.uuid) + + @mock.patch('libvirt.open', autospec=True) + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_set_secure_boot_on(self, libvirt_mock, libvirt_rw_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35_uefi.xml', + 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + self.test_driver.set_secure_boot(self.uuid, True) + + conn_mock = libvirt_rw_mock.return_value + xml_document = conn_mock.defineXML.call_args[0][0] + tree = ET.fromstring(xml_document) + os_element = tree.find('os') + loader_element = os_element.find('loader') + nvram_element = os_element.find('nvram') + self.assertEqual('yes', loader_element.get('secure')) + self.assertEqual('/usr/share/OVMF/OVMF_CODE.secboot.fd', + loader_element.text) + self.assertEqual('/usr/share/OVMF/OVMF_VARS.secboot.fd', + nvram_element.get('template')) + self.assertEqual('/var/lib/libvirt/nvram-%s.secboot.fd' % + self.uuid, nvram_element.text) + + @mock.patch('libvirt.open', autospec=True) + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_set_secure_boot_off(self, libvirt_mock, libvirt_rw_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35_uefi_secure.xml', + 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + self.test_driver.set_secure_boot(self.uuid, False) + + conn_mock = libvirt_rw_mock.return_value + xml_document = conn_mock.defineXML.call_args[0][0] + tree = ET.fromstring(xml_document) + os_element = tree.find('os') + loader_element = os_element.find('loader') + nvram_element = os_element.find('nvram') + self.assertEqual('no', loader_element.get('secure')) + self.assertEqual('/usr/share/OVMF/OVMF_CODE.secboot.fd', + loader_element.text) + self.assertEqual('/usr/share/OVMF/OVMF_VARS.fd', + nvram_element.get('template')) + self.assertEqual('/var/lib/libvirt/nvram-%s.fd' % + self.uuid, nvram_element.text) + + @mock.patch('libvirt.open', autospec=True) + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_set_secure_boot_not_uefi(self, libvirt_mock, libvirt_rw_mock): + with open('sushy_tools/tests/unit/emulator/domain-q35.xml', 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + domain_mock = conn_mock.lookupByUUID.return_value + domain_mock.XMLDesc.return_value = data + + self.assertRaises(error.NotSupportedError, + self.test_driver.set_secure_boot, self.uuid, True) diff --git a/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py b/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py index a620a414..9b10a0f9 100644 --- a/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py +++ b/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py @@ -262,3 +262,28 @@ class NovaDriverTestCase(base.BaseTestCase): self.assertRaises( error.FishyError, self.test_driver.get_simple_storage_collection, self.uuid) + + def test_get_secure_boot_off(self): + server = mock.Mock(id=self.uuid, image=dict(id=self.uuid)) + self.nova_mock.return_value.get_server.return_value = server + + image = mock.Mock() + + self.nova_mock.return_value.image.find_image.return_value = image + + self.assertFalse(self.test_driver.get_secure_boot(self.uuid)) + + def test_get_secure_boot_on(self): + server = mock.Mock(id=self.uuid, image=dict(id=self.uuid)) + self.nova_mock.return_value.get_server.return_value = server + + image = mock.Mock(os_secure_boot='required') + + self.nova_mock.return_value.image.find_image.return_value = image + + self.assertTrue(self.test_driver.get_secure_boot(self.uuid)) + + def test_set_secure_boot(self): + self.assertRaises( + error.NotSupportedError, self.test_driver.set_secure_boot, + self.uuid, True) diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index 2a878635..9bc43132 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -428,6 +428,40 @@ class EthernetInterfacesTestCase(EmulatorTestCase): self.assertEqual(404, response.status_code) +@patch_resource('systems') +class SecureBootTestCase(EmulatorTestCase): + + def test_secure_boot_get(self, systems_mock): + systems_mock = systems_mock.return_value + systems_mock.get_secure_boot.return_value = True + response = self.app.get('redfish/v1/Systems/%s/SecureBoot' % self.uuid) + + self.assertEqual(200, response.status_code) + self.assertEqual('UEFI Secure Boot', + response.json['Name']) + self.assertTrue(response.json['SecureBootEnable']) + + systems_mock.get_secure_boot.return_value = False + response = self.app.get('redfish/v1/Systems/%s/SecureBoot' % self.uuid) + self.assertFalse(response.json['SecureBootEnable']) + + def test_secure_boot_patch_on(self, systems_mock): + systems_mock = systems_mock.return_value + data = {'SecureBootEnable': True} + response = self.app.patch('redfish/v1/Systems/%s/SecureBoot' + % self.uuid, json=data) + self.assertEqual(204, response.status_code) + systems_mock.set_secure_boot.assert_called_once_with(self.uuid, True) + + def test_secure_boot_patch_off(self, systems_mock): + systems_mock = systems_mock.return_value + data = {'SecureBootEnable': False} + response = self.app.patch('redfish/v1/Systems/%s/SecureBoot' + % self.uuid, json=data) + self.assertEqual(204, response.status_code) + systems_mock.set_secure_boot.assert_called_once_with(self.uuid, False) + + @patch_resource('systems') class StorageTestCase(EmulatorTestCase):