Add BIOS resource support

Emulates BIOS support for libvirt driver by storing custom
XML with BIOS attributes in libvirt Domain XML.

For now it supports getting attributes, updating
attributes, resetting to default.

This does not support changing password yet, but can be added
later.

This does not support getting attribute update status, because
this is not implemented in sushy library yet. Can be added after
this is implemented in sushy.

Test structure depends and is based on other patches to be merged:
I02725332df886fdf6b13c98cb6d80ab3650575b9 and
I82032fd13a74d9aa77616dac3802d506b4a9c1cf.

Change-Id: I3b9e7683934e4448bedf3933f1a2fe222d70c208
Story: 2001791
Task: 22264
This commit is contained in:
Aija Jaunteva 2018-07-06 14:14:56 +03:00
parent f5450bb8c6
commit acd170e61e
11 changed files with 552 additions and 1 deletions

View File

@ -119,3 +119,28 @@ class AbstractDriver(object):
:returns: available CPU count as `int` or `None` if CPU count
can't be determined
"""
@abc.abstractmethod
def get_bios(self, identity):
"""Get BIOS attributes for the system
:returns: key-value pairs of BIOS attributes
:raises: `FishyError` if BIOS attributes cannot be processed
"""
@abc.abstractmethod
def set_bios(self, identity, attributes):
"""Update BIOS attributes
:param attributes: key-value pairs of attributes to update
:raises: `FishyError` if BIOS attributes cannot be processed
"""
@abc.abstractmethod
def reset_bios(self, identity):
"""Reset BIOS attributes to default
:raises: `FishyError` if BIOS attributes cannot be processed
"""

View File

@ -15,6 +15,7 @@
import xml.etree.ElementTree as ET
from collections import namedtuple
from sushy_tools.emulator.drivers.base import AbstractDriver
from sushy_tools.error import FishyError
@ -66,6 +67,11 @@ class LibvirtDriver(AbstractDriver):
BOOT_MODE_MAP_REV = {v: k for k, v in BOOT_MODE_MAP.items()}
DEFAULT_BIOS_ATTRIBUTES = {"BootMode": "Uefi",
"EmbeddedSata": "Raid",
"NicBoot1": "NetworkBoot",
"ProcTurboMode": "Enabled"}
def __init__(self, uri=None):
self._uri = uri or self.LIBVIRT_URI
@ -324,3 +330,143 @@ class LibvirtDriver(AbstractDriver):
total_cpus = int(vcpu_element.text)
return total_cpus or None
def _process_bios_attributes(self,
domain_xml,
bios_attributes=DEFAULT_BIOS_ATTRIBUTES,
update_existing_attributes=False):
"""Process Libvirt domain XML for BIOS attributes
This method supports adding default BIOS attributes,
retrieving existing BIOS attributes and
updating existing BIOS attributes.
This method is introduced to make XML testable otherwise have to
compare XML strings to test if XML saved to libvirt is as expected.
Sample of custom XML:
<domain type="kvm">
[...]
<metadata xmlns:sushy="http://openstack.org/xmlns/libvirt/sushy">
<sushy:bios>
<sushy:attributes>
<sushy:attribute name="ProcTurboMode" value="Enabled"/>
<sushy:attribute name="BootMode" value="Uefi"/>
<sushy:attribute name="NicBoot1" value="NetworkBoot"/>
<sushy:attribute name="EmbeddedSata" value="Raid"/>
</sushy:attributes>
</sushy:bios>
</metadata>
[...]
:param domain_xml: Libvirt domain XML to process
:param bios_attributes: BIOS attributes for updates or default
values if not specified
:param update_existing_attributes: Update existing BIOS attributes
:returns: namedtuple of tree: processed XML element tree,
attributes_written: if changes were made to XML,
bios_attributes: dict of BIOS attributes
"""
namespace = 'http://openstack.org/xmlns/libvirt/sushy'
ET.register_namespace('sushy', namespace)
ns = {'sushy': namespace}
tree = ET.fromstring(domain_xml)
metadata = tree.find('metadata')
if metadata is None:
metadata = ET.SubElement(tree, 'metadata')
bios = metadata.find('sushy:bios', ns)
attributes_written = False
if bios is not None and update_existing_attributes:
metadata.remove(bios)
bios = None
if bios is None:
bios = ET.SubElement(metadata, '{%s}bios' % (namespace))
attributes = ET.SubElement(bios, '{%s}attributes' % (namespace))
for key, value in sorted(bios_attributes.items()):
ET.SubElement(attributes,
'{%s}attribute' % (namespace),
name=key,
value=value)
attributes_written = True
bios_attributes = {atr.attrib['name']: atr.attrib['value']
for atr in tree.find('.//sushy:attributes', ns)}
BiosProcessResult = namedtuple('BiosProcessResult',
'tree, attributes_written,'
'bios_attributes')
return BiosProcessResult(tree, attributes_written, bios_attributes)
def _process_bios(self, identity,
bios_attributes=DEFAULT_BIOS_ATTRIBUTES,
update_existing_attributes=False):
"""Process Libvirt domain XML for BIOS attributes and update it if necessary
:param identity: libvirt domain name or ID
:param bios_attributes: Full list of BIOS attributes to use if
they are missing or update necessary
:param update: Update existing BIOS attributes
:returns: New or existing dict of BIOS attributes
:raises: `FishyError` if BIOS attributes cannot be saved
"""
with libvirt_open(self._uri) as conn:
libvirt_domain = conn.lookupByName(identity)
result = self._process_bios_attributes(libvirt_domain.XMLDesc(),
bios_attributes,
update_existing_attributes)
if result.attributes_written:
try:
conn.defineXML(ET.tostring(result.tree).decode('utf-8'))
except libvirt.libvirtError as e:
msg = ('Error updating BIOS attributes'
' at libvirt URI "%(uri)s": '
'%(error)s' % {'uri': self._uri, 'error': e})
raise FishyError(msg)
return result.bios_attributes
def get_bios(self, identity):
"""Get BIOS section
If there are no BIOS attributes, domain is updated with default values.
:param identity: libvirt domain name or ID
:returns: dict of BIOS attributes
"""
return self._process_bios(identity)
def set_bios(self, identity, attributes):
"""Update BIOS attributes
These values do not have any effect on VM. This is a workaround
because there is no libvirt API to manage BIOS settings.
By storing fake BIOS attributes they are attached to VM and are
persisted through VM lifecycle.
Updates to attributes are immediate unlike in real BIOS that
would require system reboot.
:param identity: libvirt domain name or ID
:param attributes: dict of BIOS attributes to update. Can pass only
attributes that need update, not all
"""
bios_attributes = self.get_bios(identity)
bios_attributes.update(attributes)
self._process_bios(identity, bios_attributes,
update_existing_attributes=True)
def reset_bios(self, identity):
"""Reset BIOS attributes to default
:param identity: libvirt domain name or ID
"""
self._process_bios(identity, self.DEFAULT_BIOS_ATTRIBUTES,
update_existing_attributes=True)

View File

@ -272,3 +272,15 @@ class OpenStackDriver(AbstractDriver):
return
return flavor.vcpus
def get_bios(self, identity):
"""Not supported as Openstack SDK does not expose API for BIOS"""
raise NotImplementedError
def set_bios(self, identity, attributes):
"""Not supported as Openstack SDK does not expose API for BIOS"""
raise NotImplementedError
def reset_bios(self, identity):
"""Not supported as Openstack SDK does not expose API for BIOS"""
raise NotImplementedError

View File

@ -15,6 +15,7 @@
import argparse
import functools
import json
import os
import ssl
import sys
@ -178,6 +179,56 @@ def system_reset_action(identity):
return '', 204
@app.route('/redfish/v1/Systems/<identity>/BIOS', methods=['GET'])
@init_virt_driver
@returns_json
def bios(identity):
bios = driver.get_bios(identity)
app.logger.debug('Serving BIOS for system "%s"', identity)
return flask.render_template(
'bios.json',
identity=identity,
bios_current_attributes=json.dumps(bios, sort_keys=True, indent=6))
@app.route('/redfish/v1/Systems/<identity>/BIOS/Settings',
methods=['GET', 'PATCH'])
@init_virt_driver
@returns_json
def bios_settings(identity):
if flask.request.method == 'GET':
bios = driver.get_bios(identity)
app.logger.debug('Serving BIOS Settings for system "%s"', identity)
return flask.render_template(
'bios_settings.json',
identity=identity,
bios_pending_attributes=json.dumps(bios, sort_keys=True, indent=6))
elif flask.request.method == 'PATCH':
attributes = flask.request.json.get('Attributes')
driver.set_bios(identity, attributes)
app.logger.info('System "%s" BIOS attributes "%s" updated',
identity, attributes)
return '', 204
@app.route('/redfish/v1/Systems/<identity>/BIOS/Actions/Bios.ResetBios',
methods=['POST'])
@init_virt_driver
@returns_json
def system_reset_bios(identity):
driver.reset_bios(identity)
app.logger.info('BIOS for system "%s" reset', identity)
return '', 204
def parse_args():
parser = argparse.ArgumentParser('sushy-emulator')
parser.add_argument('-i', '--interface',

View File

@ -0,0 +1,23 @@
{
"@odata.type": "#Bios.v1_0_0.Bios",
"Id": "BIOS",
"Name": "BIOS Configuration Current Settings",
"AttributeRegistry": "BiosAttributeRegistryP89.v1_0_0",
"Attributes": {{ bios_current_attributes }},
"@Redfish.Settings": {
"@odata.type": "#Settings.v1_0_0.Settings",
"SettingsObject": {
"@odata.id": "/redfish/v1/Systems/{{ identity }}/BIOS/Settings"
}
},
"Actions": {
"#Bios.ResetBios": {
"target": "/redfish/v1/Systems/{{ identity }}/BIOS/Actions/Bios.ResetBios"
},
"#Bios.ChangePassword": {
"target": "/redfish/v1/Systems/{{ identity }}/BIOS/Actions/Bios.ChangePassword"
}
},
"@odata.context": "/redfish/v1/$metadata#Bios.Bios",
"@odata.id": "/redfish/v1/Systems/{{ identity }}/BIOS"
}

View File

@ -0,0 +1,10 @@
{
"@odata.type": "#Bios.v1_0_0.Bios",
"Id": "Settings",
"Name": "BIOS Configuration Pending Settings",
"AttributeRegistry": "BiosAttributeRegistryP89.v1_0_0",
"Attributes": {{ bios_pending_attributes }},
"@odata.context": "/redfish/v1/$metadata#Bios.Bios",
"@odata.id": "/redfish/v1/Systems/{{ identity }}/BIOS/Settings",
"@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -0,0 +1,29 @@
<domain type='qemu'
xmlns:sushy="http://openstack.org/xmlns/libvirt/sushy">
<name>QEmu-fedora-i686</name>
<uuid>c7a5fdbd-cdaf-9455-926a-d65c16db1809</uuid>
<memory>219200</memory>
<currentMemory>219200</currentMemory>
<vcpu>2</vcpu>
<os>
<type arch='i686' machine='pc'>hvm</type>
<boot dev='cdrom'/>
<loader type='rom'/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/home/user/boot.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<source file='/home/user/fedora.img'/>
<target dev='hda'/>
</disk>
<interface type='network'>
<source network='default'/>
</interface>
<graphics type='vnc' port='-1'/>
</devices>
<metadata><sushy:bios><sushy:attributes><sushy:attribute name="BootMode" value="Bios" /><sushy:attribute name="EmbeddedSata" value="Raid" /><sushy:attribute name="NicBoot1" value="NetworkBoot" /><sushy:attribute name="ProcTurboMode" value="Disabled" /></sushy:attributes></sushy:bios></metadata></domain>

View File

@ -0,0 +1,29 @@
<domain type='qemu'>
<name>QEmu-fedora-i686</name>
<uuid>c7a5fdbd-cdaf-9455-926a-d65c16db1809</uuid>
<memory>219200</memory>
<currentMemory>219200</currentMemory>
<vcpu>2</vcpu>
<os>
<type arch='i686' machine='pc'>hvm</type>
<boot dev='cdrom'/>
<loader type='rom'/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/home/user/boot.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<source file='/home/user/fedora.img'/>
<target dev='hda'/>
</disk>
<interface type='network'>
<source network='default'/>
</interface>
<graphics type='vnc' port='-1'/>
</devices>
<metadata /></domain>

View File

@ -11,11 +11,14 @@
# under the License.
import json
import libvirt
from oslotest import base
from six.moves import mock
from sushy_tools.emulator.drivers.libvirtdriver import LibvirtDriver
from sushy_tools.emulator import main
from sushy_tools.error import FishyError
class EmulatorTestCase(base.BaseTestCase):
@ -25,7 +28,7 @@ class EmulatorTestCase(base.BaseTestCase):
# This enables libvirt driver
main.driver = None
self.test_driver = LibvirtDriver()
super(EmulatorTestCase, self).setUp()
def test_root_resource(self):
@ -186,3 +189,131 @@ class EmulatorTestCase(base.BaseTestCase):
data=data, content_type='application/json')
self.assertEqual(response.status, '204 NO CONTENT')
domain_mock.injectNMI.assert_called_once_with()
@mock.patch('libvirt.open', autospec=True)
def test_get_bios(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain.xml') as f:
domain_xml = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByName.return_value
domain_mock.XMLDesc.return_value = domain_xml
bios_attributes = self.test_driver.get_bios('xxx-yyy-zzz')
self.assertEqual(LibvirtDriver.DEFAULT_BIOS_ATTRIBUTES,
bios_attributes)
self.assertEqual(1, conn_mock.defineXML.call_count)
@mock.patch('libvirt.open', autospec=True)
def test_get_bios_existing(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain_bios.xml') as f:
domain_xml = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByName.return_value
domain_mock.XMLDesc.return_value = domain_xml
bios_attributes = self.test_driver.get_bios('xxx-yyy-zzz')
self.assertEqual({"BootMode": "Bios",
"EmbeddedSata": "Raid",
"NicBoot1": "NetworkBoot",
"ProcTurboMode": "Disabled"},
bios_attributes)
conn_mock.defineXML.assert_not_called()
@mock.patch('libvirt.open', autospec=True)
def test_set_bios(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain_bios.xml') as f:
domain_xml = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByName.return_value
domain_mock.XMLDesc.return_value = domain_xml
self.test_driver.set_bios('xxx-yyy-zzz',
{"BootMode": "Uefi",
"ProcTurboMode": "Enabled"})
self.assertEqual(1, conn_mock.defineXML.call_count)
@mock.patch('libvirt.open', autospec=True)
def test_reset_bios(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain_bios.xml') as f:
domain_xml = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByName.return_value
domain_mock.XMLDesc.return_value = domain_xml
self.test_driver.reset_bios('xxx-yyy-zzz')
self.assertEqual(1, conn_mock.defineXML.call_count)
def test__process_bios_attributes_get_default(self):
with open('sushy_tools/tests/unit/emulator/domain.xml') as f:
domain_xml = f.read()
result = self.test_driver._process_bios_attributes(domain_xml)
self.assertTrue(result.attributes_written)
self.assertEqual(LibvirtDriver.DEFAULT_BIOS_ATTRIBUTES,
result.bios_attributes)
self._assert_bios_xml(result.tree)
def test__process_bios_attributes_get_default_metadata_exists(self):
with open('sushy_tools/tests/unit/emulator/'
'domain_metadata.xml') as f:
domain_xml = f.read()
result = self.test_driver._process_bios_attributes(domain_xml)
self.assertTrue(result.attributes_written)
self.assertEqual(LibvirtDriver.DEFAULT_BIOS_ATTRIBUTES,
result.bios_attributes)
self._assert_bios_xml(result.tree)
def test__process_bios_attributes_get_existing(self):
with open('sushy_tools/tests/unit/emulator/domain_bios.xml') as f:
domain_xml = f.read()
result = self.test_driver._process_bios_attributes(domain_xml)
self.assertFalse(result.attributes_written)
self.assertEqual({"BootMode": "Bios",
"EmbeddedSata": "Raid",
"NicBoot1": "NetworkBoot",
"ProcTurboMode": "Disabled"},
result.bios_attributes)
self._assert_bios_xml(result.tree)
def test__process_bios_attributes_update(self):
with open('sushy_tools/tests/unit/emulator/domain_bios.xml') as f:
domain_xml = f.read()
result = self.test_driver._process_bios_attributes(
domain_xml,
{"BootMode": "Uefi",
"ProcTurboMode": "Enabled"},
True)
self.assertTrue(result.attributes_written)
self.assertEqual({"BootMode": "Uefi",
"ProcTurboMode": "Enabled"},
result.bios_attributes)
self._assert_bios_xml(result.tree)
def _assert_bios_xml(self, tree):
ns = {'sushy': 'http://openstack.org/xmlns/libvirt/sushy'}
self.assertIsNotNone(tree.find('metadata')
.find('sushy:bios', ns)
.find('sushy:attributes', ns))
@mock.patch('libvirt.open', autospec=True)
def test__process_bios_error(self, libvirt_mock):
with open('sushy_tools/tests/unit/emulator/domain.xml') as f:
domain_xml = f.read()
conn_mock = libvirt_mock.return_value
domain_mock = conn_mock.lookupByName.return_value
domain_mock.XMLDesc.return_value = domain_xml
conn_mock.defineXML.side_effect = libvirt.libvirtError(
'because I can')
self.assertRaises(FishyError,
self.test_driver._process_bios,
'xxx-yyy-zzz',
{"BootMode": "Uefi",
"ProcTurboMode": "Enabled"})

View File

@ -0,0 +1,70 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from oslotest import base
from six.moves import mock
from sushy_tools.emulator import main
@mock.patch('sushy_tools.emulator.main.driver', autospec=True)
class EmulatorTestCase(base.BaseTestCase):
def setUp(self):
main.driver = None
self.app = main.app.test_client()
super(EmulatorTestCase, self).setUp()
def test_bios(self, driver_mock):
driver_mock.get_bios.return_value = {"attribute 1": "value 1",
"attribute 2": "value 2"}
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS')
self.assertEqual('200 OK', response.status)
self.assertEqual('BIOS', response.json['Id'])
self.assertEqual({"attribute 1": "value 1",
"attribute 2": "value 2"},
response.json['Attributes'])
def test_bios_settings(self, driver_mock):
driver_mock.get_bios.return_value = {"attribute 1": "value 1",
"attribute 2": "value 2"}
response = self.app.get(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings')
self.assertEqual('200 OK', response.status)
self.assertEqual('Settings', response.json['Id'])
self.assertEqual({"attribute 1": "value 1",
"attribute 2": "value 2"},
response.json['Attributes'])
def test_bios_settings_patch(self, driver_mock):
self.app.driver = driver_mock
response = self.app.patch(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS/Settings',
data=json.dumps({'Attributes': {'key': 'value'}}),
content_type='application/json')
self.assertEqual('204 NO CONTENT', response.status)
driver_mock.set_bios.assert_called_once_with('xxxx-yyyy-zzzz',
{'key': 'value'})
def test_reset_bios(self, driver_mock):
self.app.driver = driver_mock
response = self.app.post('/redfish/v1/Systems/xxxx-yyyy-zzzz/'
'BIOS/Actions/Bios.ResetBios')
self.assertEqual('204 NO CONTENT', response.status)
driver_mock.reset_bios.assert_called_once_with('xxxx-yyyy-zzzz')

View File

@ -18,6 +18,7 @@ import os
from oslotest import base
from six.moves import mock
from sushy_tools.emulator.drivers.novadriver import OpenStackDriver
from sushy_tools.emulator import main
@ -157,3 +158,27 @@ class EmulatorTestCase(base.BaseTestCase):
json=data)
self.assertEqual(response.status_code, 204)
server.compute.reboot_server.called_once()
@mock.patch('openstack.connect', autospec=True)
def test_get_bios(self, nova_mock):
test_driver = OpenStackDriver('fake-cloud')
self.assertRaises(
NotImplementedError,
test_driver.get_bios, 'xxx-yyy-zzz')
@mock.patch('openstack.connect', autospec=True)
def test_set_bios(self, nova_mock):
test_driver = OpenStackDriver('fake-cloud')
self.assertRaises(
NotImplementedError,
test_driver.set_bios,
'xxx-yyy-zzz',
{'attribute 1': 'value 1'})
@mock.patch('openstack.connect', autospec=True)
def test_reset_bios(self, nova_mock):
test_driver = OpenStackDriver('fake-cloud')
self.assertRaises(
NotImplementedError,
test_driver.reset_bios,
'xxx-yyy-zzz')