sushy-tools/sushy_tools/emulator/resources/systems/libvirtdriver.py

1662 lines
60 KiB
Python

# Copyright 2018 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
from collections import defaultdict
from collections import namedtuple
import os
import uuid
import xml.etree.ElementTree as ET
from sushy_tools.emulator import constants
from sushy_tools.emulator import memoize
from sushy_tools.emulator.resources.systems.base import AbstractSystemsDriver
from sushy_tools import error
try:
import libvirt
except ImportError:
libvirt = None
is_loaded = bool(libvirt)
BiosProcessResult = namedtuple('BiosProcessResult',
['tree',
'attributes_written',
'bios_attributes'])
FirmwareProcessResult = namedtuple('FirmwareProcessResult',
['tree',
'attributes_written',
'firmware_versions'])
class libvirt_open(object):
def __init__(self, uri, readonly=False):
self._uri = uri
self._readonly = readonly
def __enter__(self):
try:
self._conn = (libvirt.openReadOnly(self._uri)
if self._readonly else
libvirt.open(self._uri))
return self._conn
except libvirt.libvirtError as e:
msg = ('Error when connecting to the libvirt URI "%(uri)s": '
'%(error)s' % {'uri': self._uri, 'error': e})
raise error.FishyError(msg)
def __exit__(self, type, value, traceback):
self._conn.close()
class LibvirtDriver(AbstractSystemsDriver):
"""Libvirt driver"""
# XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS
BOOT_DEVICE_MAP = {
constants.DEVICE_TYPE_PXE: 'network',
constants.DEVICE_TYPE_HDD: 'hd',
constants.DEVICE_TYPE_CD: 'cdrom',
constants.DEVICE_TYPE_FLOPPY: 'floppy'
}
BOOT_DEVICE_MAP_REV = {v: k for k, v in BOOT_DEVICE_MAP.items()}
DISK_DEVICE_MAP = {
constants.DEVICE_TYPE_HDD: 'disk',
constants.DEVICE_TYPE_CD: 'cdrom',
constants.DEVICE_TYPE_FLOPPY: 'floppy'
}
DISK_DEVICE_MAP_REV = {v: k for k, v in DISK_DEVICE_MAP.items()}
INTERFACE_MAP = {
constants.DEVICE_TYPE_PXE: 'network',
}
INTERFACE_MAP_REV = {v: k for k, v in INTERFACE_MAP.items()}
LIBVIRT_URI = 'qemu:///system'
BOOT_MODE_AUTO_FW_MAP = {
'UEFI': 'efi',
'Legacy': 'bios'
}
BOOT_MODE_AUTO_FW_MAP_REV = {v: k for k, v
in BOOT_MODE_AUTO_FW_MAP.items()}
BOOT_MODE_MAP = {
'Legacy': 'rom',
'UEFI': 'pflash'
}
BOOT_MODE_MAP_REV = {v: k for k, v in BOOT_MODE_MAP.items()}
BOOT_LOADER_MAP = {
'UEFI': {
'x86_64': '/usr/share/OVMF/OVMF_CODE.secboot.fd',
'aarch64': '/usr/share/AAVMF/AAVMF_CODE.fd'
},
'Legacy': {
'x86_64': None,
'aarch64': None
}
}
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',
}
DEVICE_TYPE_MAP_REV = {v: k for k, v in DEVICE_TYPE_MAP.items()}
# target device, controller ID for libvirt domain
DEVICE_TARGET_MAP = {
constants.DEVICE_TYPE_FLOPPY: ('fda', 'fdc'),
constants.DEVICE_TYPE_CD: ('hdc', 'ide'),
}
DEFAULT_FIRMWARE_VERSIONS = {"BiosVersion": "1.0.0"}
DEFAULT_BIOS_ATTRIBUTES = {"BootMode": "Uefi",
"EmbeddedSata": "Raid",
"L2Cache": "10x256 KB",
"NicBoot1": "NetworkBoot",
"NumCores": "10",
"QuietBoot": "true",
"ProcTurboMode": "Enabled",
"SecureBootStatus": "Enabled",
"SerialNumber": "QPX12345",
"SysPassword": ""}
STORAGE_POOL = 'default'
STORAGE_VOLUME_XML = """
<volume type='file'>
<name>%(name)s</name>
<key>%(path)s</key>
<capacity unit='bytes'>%(size)i</capacity>
<physical unit='bytes'>%(size)i</physical>
<target>
<path>%(path)s</path>
<format type='raw'/>
</target>
</volume>
"""
@classmethod
def initialize(cls, config, logger, uri=None, *args, **kwargs):
cls._config = config
cls._logger = logger
cls._uri = uri or cls.LIBVIRT_URI
cls.BOOT_LOADER_MAP = cls._config.get(
'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)
cls._http_boot_uri = None
return cls
@memoize.memoize()
def _get_domain(self, identity, readonly=False):
with libvirt_open(self._uri, readonly=readonly) as conn:
try:
uu_identity = uuid.UUID(identity)
return conn.lookupByUUID(uu_identity.bytes)
except (ValueError, libvirt.libvirtError):
try:
domain = conn.lookupByName(identity)
except libvirt.libvirtError as ex:
msg = ('Error finding domain by name/UUID "%(identity)s" '
'at libvirt URI %(uri)s": %(err)s' %
{'identity': identity,
'uri': self._uri, 'err': ex})
self._logger.debug(msg)
raise error.NotFound(msg)
raise error.AliasAccessError(domain.UUIDString())
# Copied from nova/virt/libvirt/guest.py
def get_xml_desc(self, domain, dump_inactive=True,
dump_sensitive=True):
"""Returns xml description of guest.
:param dump_inactive: Dump inactive domain information
:param domain: The libvirt domain to call
:param dump_sensitive: Dump security sensitive information
:returns string: XML description of the guest
"""
flags = dump_inactive and libvirt.VIR_DOMAIN_XML_INACTIVE or 0
flags |= dump_sensitive and libvirt.VIR_DOMAIN_XML_SECURE or 0
return domain.XMLDesc(flags=flags)
@property
def driver(self):
"""Return human-friendly driver information
:returns: driver information as string
"""
return '<libvirt>'
@property
def systems(self):
"""Return available computer systems
:returns: list of UUIDs representing the systems
"""
with libvirt_open(self._uri, readonly=True) as conn:
return [domain.UUIDString() for domain in conn.listAllDomains()]
def uuid(self, identity):
"""Get computer system UUID
The universal unique identifier (UUID) for this system. Can be used
in place of system name if there are duplicates.
:param identity: libvirt domain name or UUID
:raises: NotFound if the system cannot be found
:returns: computer system UUID
"""
domain = self._get_domain(identity, readonly=True)
return domain.UUIDString()
def name(self, identity):
"""Get computer system name by name
:param identity: libvirt domain name or UUID
:raises: NotFound if the system cannot be found
:returns: computer system name
"""
domain = self._get_domain(identity, readonly=True)
return domain.name()
def get_power_state(self, identity):
"""Get computer system power state
:param identity: libvirt domain name or ID
:returns: current power state as *On* or *Off* `str` or `None`
if power state can't be determined
"""
domain = self._get_domain(identity, readonly=True)
return 'On' if domain.isActive() else 'Off'
def set_power_state(self, identity, state):
"""Set computer system power state
:param identity: libvirt domain name or ID
:param state: string literal requesting power state transition.
Valid values are: *On*, *ForceOn*, *ForceOff*, *GracefulShutdown*,
*GracefulRestart*, *ForceRestart*, *Nmi*.
:raises: `error.FishyError` if power state can't be set
"""
domain = self._get_domain(identity)
try:
if state in ('On', 'ForceOn'):
if not domain.isActive():
domain.create()
elif state == 'ForceOff':
if domain.isActive():
domain.destroy()
elif state == 'GracefulShutdown':
if domain.isActive():
domain.shutdown()
elif state == 'GracefulRestart':
if domain.isActive():
domain.reboot()
elif state == 'ForceRestart':
if domain.isActive():
domain.reset()
elif state == 'Nmi':
if domain.isActive():
domain.injectNMI()
except libvirt.libvirtError as e:
msg = ('Error changing power state at libvirt URI "%(uri)s": '
'%(error)s' % {'uri': self._uri, 'error': e})
raise error.FishyError(msg)
def get_boot_device(self, identity):
"""Get computer system boot device name
First try to get boot device from bootloader configuration.. If it's
not present, proceed towards gathering boot order information from
per-device boot configuration, then pick the lowest ordered device.
:param identity: libvirt domain name or ID
:returns: boot device name as `str` or `None` if device name
can't be determined
"""
# If not setting Boot devices then just report HDD
if self.SUSHY_EMULATOR_IGNORE_BOOT_DEVICE:
return constants.DEVICE_TYPE_HDD
domain = self._get_domain(identity, readonly=True)
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
# Try boot configuration in the bootloader
boot_element = tree.find('.//boot')
if boot_element is not None:
dev_attr = boot_element.get('dev')
if dev_attr is not None:
boot_source_target = self.BOOT_DEVICE_MAP_REV.get(dev_attr)
if boot_source_target:
return boot_source_target
min_order = boot_source_target = None
# If bootloader config is not present, try per-device boot elements
devices_element = tree.find('devices')
if devices_element is not None:
for disk_element in devices_element.findall('disk'):
boot_element = disk_element.find('boot')
if boot_element is None:
continue
order = boot_element.get('order')
if not order:
continue
order = int(order)
if min_order is not None and order >= min_order:
continue
device_attr = disk_element.get('device')
if device_attr is None:
continue
boot_source_target = self.DISK_DEVICE_MAP_REV.get(
device_attr)
if boot_source_target:
min_order = order
for interface_element in devices_element.findall('interface'):
boot_element = interface_element.find('boot')
if boot_element is None:
continue
order = boot_element.get('order')
if not order:
continue
order = int(order)
if min_order is not None and order >= min_order:
continue
boot_source_target = self.INTERFACE_MAP_REV.get('network')
if boot_source_target:
min_order = order
return boot_source_target
def _defineDomain(self, tree):
try:
with libvirt_open(self._uri) as conn:
conn.defineXML(ET.tostring(tree).decode('utf-8'))
except libvirt.libvirtError as e:
msg = ('Error changing boot device at libvirt URI "%(uri)s": '
'%(error)s' % {'uri': self._uri, 'error': e})
raise error.FishyError(msg)
def set_boot_device(self, identity, boot_source):
"""Get/Set computer system boot device name
First remove all boot device configuration from bootloader because
that's legacy with libvirt. Then remove possible boot configuration
in the per-device settings. Finally, make the desired boot device
the only bootable by means of per-device configuration boot option.
:param identity: libvirt domain name or ID
:param boot_source: string literal requesting boot device
change on the system. Valid values are: *Pxe*, *Hdd*, *Cd*.
:raises: `error.FishyError` if boot device can't be set
"""
domain = self._get_domain(identity)
# XML schema: https://libvirt.org/formatdomain.html#elementsOSBIOS
tree = ET.fromstring(self.get_xml_desc(domain))
# Remove bootloader configuration
os_element_order = []
for os_element in tree.findall('os'):
for boot_element in os_element.findall('boot'):
os_element_order.append(boot_element.get('dev'))
os_element.remove(boot_element)
if self.SUSHY_EMULATOR_IGNORE_BOOT_DEVICE:
self._logger.warning('Ignoring setting of boot device')
boot_element = ET.SubElement(os_element, 'boot')
boot_element.set('dev', 'fd')
self._defineDomain(tree)
return
target = self.DISK_DEVICE_MAP.get(boot_source)
# Process per-device boot configuration
devices_element = tree.find('devices')
if devices_element is None:
msg = ('Incomplete libvirt domain configuration - <devices> '
'element is missing in domain '
'%(uuid)s' % {'uuid': domain.UUIDString()})
raise error.FishyError(msg)
target_device_elements = []
cur_hd_osboot_elements = []
cur_hd_order_elements = []
# Remove per-disk boot configuration
# We should save at least hdd boot entries instead of just removing
# everything. In some scenarios PXE after provisioning stops replying
# and if there is no other boot device, then vm will fail to boot
# cdrom and floppy are ignored.
for disk_element in devices_element.findall('disk'):
device_attr = disk_element.get('device')
if device_attr is None:
continue
boot_elements = disk_element.findall('boot')
# NOTE(etingof): multiple devices of the same type not supported
if device_attr == target:
target_device_elements.append(disk_element)
elif 'hd' in os_element_order:
cur_hd_osboot_elements.append(disk_element)
elif boot_elements:
cur_hd_order_elements.append(disk_element)
for boot_element in boot_elements:
disk_element.remove(boot_element)
target = self.INTERFACE_MAP.get(boot_source)
# Remove per-interface boot configuration
for interface_element in devices_element.findall('interface'):
if target == 'network':
target_device_elements.append(interface_element)
for boot_element in interface_element.findall('boot'):
interface_element.remove(boot_element)
if not target_device_elements:
msg = ('Target libvirt device %(target)s does not exist in domain '
'%(uuid)s' % {'target': boot_source,
'uuid': domain.UUIDString()})
raise error.FishyError(msg)
# OS boot and per device boot order are mutually exclusive
if cur_hd_osboot_elements:
sorted_hd_elements = sorted(
cur_hd_osboot_elements,
key=lambda child: child.find('target').get('dev'))
target_device_elements.extend(sorted_hd_elements)
else:
target_device_elements.extend(cur_hd_order_elements)
# NOTE(etingof): Make all chosen devices bootable (important for NICs)
for order, target_device_element in enumerate(target_device_elements):
boot_element = ET.SubElement(target_device_element, 'boot')
boot_element.set('order', str(order + 1))
self._defineDomain(tree)
def _is_firmware_autoselection(self, tree):
"""Get libvirt firmware autoselection mode
:param tree: libvirt domain XML tree
:returns: True if firmware autoselection is enabled
"""
os_element = tree.find('.//os')
return True if os_element.get('firmware') else False
def get_boot_mode(self, identity):
"""Get computer system boot mode.
:param identity: libvirt domain name or ID
:returns: either *UEFI* or *Legacy* as `str` or `None` if
current boot mode can't be determined
"""
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))
if self._is_firmware_autoselection(tree):
os_element = tree.find('.//os')
boot_mode = (
self.BOOT_MODE_AUTO_FW_MAP_REV.get(os_element.get('firmware'))
)
return boot_mode
loader_element = tree.find('.//loader')
if loader_element is not None:
boot_mode = (
self.BOOT_MODE_MAP_REV.get(loader_element.get('type'))
)
return boot_mode
def set_boot_mode(self, identity, boot_mode):
"""Set computer system boot mode.
:param identity: libvirt domain name or ID
:param boot_mode: string literal requesting boot mode
change on the system. Valid values are: *UEFI*, *Legacy*.
:raises: `error.FishyError` if boot mode can't be set
"""
domain = self._get_domain(identity)
# 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
:raises: `error.FishyError` if boot mode can't be set
"""
try:
loader_type = self.BOOT_MODE_MAP[boot_mode]
except KeyError:
msg = ('Unknown boot mode requested: '
'%(boot_mode)s' % {'boot_mode': boot_mode})
raise error.BadRequest(msg)
os_elements = tree.findall('os')
if len(os_elements) != 1:
msg = ('Can\'t set boot mode because "os" element must be present '
'exactly once in domain "%(identity)s" '
'configuration' % {'identity': identity})
raise error.FishyError(msg)
os_element = os_elements[0]
if self._is_firmware_autoselection(tree):
self._build_os_element_fw_autoselection(boot_mode, secure,
os_element)
else:
self._build_os_element_fw_manualselection(boot_mode, secure,
os_element, loader_type)
def _build_os_element_fw_autoselection(self, boot_mode, secure,
os_element):
"""Set the boot mode and secure boot (auto-selection)
:raises: `error.FishyError` if boot mode can't be set
"""
os_element.set('firmware', self.BOOT_MODE_AUTO_FW_MAP[boot_mode])
# Delete the secure-boot feature element
try:
firmware_element = os_element.findall('firmware').pop()
for e in firmware_element.findall('.feature'
'[@name="secure-boot"]'):
firmware_element.remove(e)
except IndexError:
firmware_element = None
if boot_mode != 'UEFI':
return
if firmware_element is None:
firmware_element = ET.SubElement(os_element, 'firmware')
if secure:
secure_boot_element = ET.SubElement(firmware_element, 'feature')
secure_boot_element.set('name', 'secure-boot')
secure_boot_element.set('enabled', 'yes')
else:
secure_boot_element = ET.SubElement(firmware_element, 'feature')
secure_boot_element.set('name', 'secure-boot')
secure_boot_element.set('enabled', 'no')
def _build_os_element_fw_manualselection(self, boot_mode, secure,
os_element, loader_type):
"""Set the boot mode and secure boot (manual-selection)
This also converts from the previous manual layout to the automatic
approach.
:raises: `error.FishyError` if boot mode can't be set
"""
type_element = os_element.find('type')
if type_element is None:
os_arch = None
else:
os_arch = type_element.get('arch')
try:
loader_path = self.BOOT_LOADER_MAP[boot_mode][os_arch]
except KeyError:
self._logger.warning(
'Boot loader binary is not configured for '
'boot mode %s and OS architecture %s. '
'Assuming default boot loader for the domain.',
boot_mode, os_arch)
loader_path = None
nvram_element = os_element.find('nvram')
nvram_path = nvram_element.text if nvram_element is not None else None
# 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 = '.nosecboot.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
if nvram_path:
nvram_file = os.path.basename(nvram_path)
# replace suffix
for suffix in ['.secboot.fd', '.nosecboot.fd', '.fd']:
# str.removesuffix() for Python <3.9
if nvram_file.endswith(suffix):
nvram_file = nvram_file[:-len(suffix)]
nvram_file += nvram_suffix
nvram_element.text = os.path.join(os.path.dirname(nvram_path),
nvram_file)
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))
if self._is_firmware_autoselection(tree):
return self._get_secureboot_fw_auto_selection(identity, tree)
else:
return self._get_secureboot_fw_manual_selection(identity, tree)
def _get_secureboot_fw_auto_selection(self, identity, tree):
os_element = tree.find('os')
firmware_element = os_element.findall('firmware')
if len(firmware_element) == 0:
msg = ('Can\'t get secure boot state because "firmware" element '
'is not present in domain "%(identity)s" configuration'
% {'identity': identity})
raise error.FishyError(msg)
if len(firmware_element) > 1:
msg = ('Can\'t get secure boot state because "firmware" element '
'must be present exactly once in domain "%(identity)s" '
'configuration' % {'identity': identity})
raise error.FishyError(msg)
feature_secure_boot = os_element.findall('./firmware/feature'
'[@name="secure-boot"]')
if len(feature_secure_boot) > 1:
msg = ('Can\'t get secure boot state because the "firmware" '
'element contains multiple "feature" elements with the '
'"secure-boot" name attribute. "secure-boot" feature '
'should be present exactly once in domain %(identity)s" '
'configuration' % {'identity': identity})
raise error.FishyError(msg)
enabled = feature_secure_boot[0].get('enabled', "no")
return True if enabled == "yes" else False
def _get_secureboot_fw_manual_selection(self, identity, tree):
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 not nvram:
return False
nvram_template = nvram[0].get('template')
return nvram_template == self.SECURE_BOOT_ENABLED_NVRAM
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
"""
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#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:
try:
conn.defineXML(ET.tostring(tree).decode('utf-8'))
except libvirt.libvirtError as e:
msg = ('Error changing secure boot at libvirt URI '
'"%(uri)s": %(error)s' % {'uri': self._uri,
'error': e})
raise error.FishyError(msg)
def get_total_memory(self, identity):
"""Get computer system total memory
:param identity: libvirt domain name or ID
:returns: available RAM in GiB as `int` or `None` if total memory
count can't be determined
"""
domain = self._get_domain(identity, readonly=True)
return int(domain.maxMemory() / 1024 / 1024)
def get_total_cpus(self, identity):
"""Get computer system total count of available CPUs
:param identity: libvirt domain name or ID
:returns: available CPU count as `int` or `None` if CPU count
can't be determined
"""
total_cpus = 0
domain = self._get_domain(identity, readonly=True)
if domain.isActive():
total_cpus = domain.maxVcpus()
# If we can't get it from maxVcpus() try to find it by
# inspecting the domain XML
if total_cpus <= 0:
tree = ET.fromstring(
domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
vcpu_element = tree.find('.//vcpu')
if vcpu_element is not None:
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 None:
bios = ET.SubElement(metadata, '{%s}bios' % (namespace))
attributes = bios.find('sushy:attributes', ns)
if attributes is not None and update_existing_attributes:
bios.remove(attributes)
attributes = None
if attributes is None:
attributes = ET.SubElement(bios, '{%s}attributes' % (namespace))
for key, value in sorted(bios_attributes.items()):
if not isinstance(value, str):
value = str(value)
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)}
return BiosProcessResult(tree, attributes_written, bios_attributes)
def _process_versions_attributes(
self,
domain_xml,
firmware_versions=DEFAULT_FIRMWARE_VERSIONS,
update_existing_attributes=False):
"""Process Libvirt domain XML for firmware version attributes
This method supports adding default firmware version information,
retrieving existing version attributes and
updating existing version 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 (attributes section retained for context
although this code doesn't manage attributes, only versions:
<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:versions>
<sushy:version name="BiosVersion" value="1.1.0"/>
</sushy:versions>
</sushy:bios>
</metadata>
[...]
:param domain_xml: Libvirt domain XML to process
:param firmware_versions: firmware version information for updates or
default values if not specified
:param update_existing_attributes: Update existing firmware version
attributes
:returns: namedtuple of tree: processed XML element tree,
attributes_written: if changes were made to XML,
versions: dict of firmware versions
"""
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 None:
bios = ET.SubElement(metadata, '{%s}bios' % (namespace))
versions = bios.find('sushy:versions', ns)
if versions is not None and update_existing_attributes:
bios.remove(versions)
versions = None
if versions is None:
versions = ET.SubElement(bios, '{%s}versions' % (namespace))
for key, value in sorted(firmware_versions.items()):
if not isinstance(value, str):
value = str(value)
ET.SubElement(versions,
'{%s}version' % (namespace),
name=key,
value=value)
attributes_written = True
firmware_versions = {ver.attrib['name']: ver.attrib['value']
for ver in tree.find('.//sushy:versions', ns)}
return FirmwareProcessResult(tree, attributes_written,
firmware_versions)
def _process_bios(self, identity,
bios_attributes=DEFAULT_BIOS_ATTRIBUTES,
update_existing_attributes=False):
"""Process Libvirt domain XML for BIOS attributes
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_existing_attributes: Update existing BIOS attributes
:returns: New or existing dict of BIOS attributes
:raises: `error.FishyError` if BIOS attributes cannot be saved
"""
domain = self._get_domain(identity)
result = self._process_bios_attributes(
domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE),
bios_attributes,
update_existing_attributes)
if result.attributes_written:
try:
with libvirt_open(self._uri) as conn:
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 error.FishyError(msg)
return result.bios_attributes
def _process_versions(self, identity,
firmware_versions=DEFAULT_FIRMWARE_VERSIONS,
update_existing_attributes=False):
"""Process Libvirt domain XML for firmware versions
Process Libvirt domain XML for firmware versions and update it if
necessary
:param identity: libvirt domain name or ID
:param firmware_versions: Full list of firmware versions to use if
they are missing or update necessary
:param update_existing_attributes: Update existing firmware versions
:returns: New or existing dict of firmware versions
:raises: `error.FishyError` if firmware versions cannot be saved
"""
domain = self._get_domain(identity)
result = self._process_versions_attributes(
domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE),
firmware_versions,
update_existing_attributes)
if result.attributes_written:
try:
with libvirt_open(self._uri) as conn:
conn.defineXML(ET.tostring(result.tree).decode('utf-8'))
except libvirt.libvirtError as e:
msg = ('Error updating firmware versions'
' at libvirt URI "%(uri)s": '
'%(error)s' % {'uri': self._uri, 'error': e})
raise error.FishyError(msg)
return result.firmware_versions
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 get_versions(self, identity):
"""Get firmware versions section
If there are no firmware version attributes, domain is updated with
default values.
:param identity: libvirt domain name or ID
:returns: dict of firmware version attributes
"""
return self._process_versions(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 set_versions(self, identity, firmware_versions):
"""Update firmware versions
These values do not have any effect on VM. This is a workaround
because there is no libvirt API to manage firmware versions.
By storing fake firmware versions they are attached to VM and are
persisted through VM lifecycle.
Updates to versions are immediate unlike in real firmware that
would require system reboot.
:param identity: libvirt domain name or ID
:param firmware_versions: dict of firmware versions to update.
Can pass only versions that need update, not all
"""
versions = self.get_versions(identity)
versions.update(firmware_versions)
self._process_versions(identity, firmware_versions,
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)
def reset_versions(self, identity):
"""Reset firmware versions to default
:param identity: libvirt domain name or ID
"""
self._process_versions(identity, self.DEFAULT_FIRMWARE_VERSIONS,
update_existing_attributes=True)
def get_nics(self, identity):
"""Get list of network interfaces and their MAC addresses
Use MAC address as network interface's id
:param identity: libvirt domain name or ID
:returns: list of network interfaces dict with their attributes
"""
domain = self._get_domain(identity, readonly=True)
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
return [{'id': iface.get('address'), 'mac': iface.get('address')}
for iface in tree.findall(
".//devices/interface/mac")]
def get_processors(self, identity):
"""Get list of processors
:param identity: libvirt domain name or ID
:returns: list of processors dict with their attributes
"""
domain = self._get_domain(identity, readonly=True)
processors_count = self.get_total_cpus(identity)
processors = [{'id': 'CPU{0}'.format(x),
'socket': 'CPU {0}'.format(x)}
for x in range(processors_count)]
tree = ET.fromstring(domain.XMLDesc())
try:
model = tree.find('.//cpu/model').text
except AttributeError:
model = 'N/A'
try:
vendor = tree.find('.//cpu/vendor').text
except AttributeError:
vendor = 'N/A'
try:
cores = tree.find('.//cpu/topology').get('cores')
threads = tree.find('.//cpu/topology').get('threads')
except AttributeError:
# still return an integer as clients are expecting
cores = '1'
threads = '1'
for processor in processors:
processor['model'] = model
processor['vendor'] = vendor
processor['cores'] = cores
processor['threads'] = threads
return processors
def get_boot_image(self, identity, device):
"""Get backend VM boot image info
:param identity: libvirt domain name or ID
:param device: device type (from
`sushy_tools.emulator.constants`)
:returns: a `tuple` of (boot_image, write_protected, inserted)
:raises: `error.FishyError` if boot device can't be accessed
"""
domain = self._get_domain(identity, readonly=True)
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
device_element = tree.find('devices')
if device_element is None:
msg = ('Missing "devices" tag in the libvirt domain '
'"%(identity)s" configuration' % {'identity': identity})
raise error.FishyError(msg)
for disk_element in device_element.findall('disk'):
dev_type = disk_element.attrib.get('device')
if (dev_type not in self.DEVICE_TYPE_MAP_REV
or dev_type != self.DEVICE_TYPE_MAP.get(device)):
continue
source_element = disk_element.find('source')
if source_element is None:
continue
boot_image = source_element.attrib.get('file')
if boot_image is None:
continue
read_only = disk_element.find('readonly') or False
inserted = (
self.get_boot_device(identity) == constants.DEVICE_TYPE_CD
)
if inserted:
inserted = self.get_boot_mode(identity) == 'UEFI'
return boot_image, read_only, inserted
return '', False, False
def _upload_image(self, domain, conn, boot_image):
pool = conn.storagePoolLookupByName(self.STORAGE_POOL)
pool_tree = ET.fromstring(pool.XMLDesc())
# Find out path to images
pool_path_element = pool_tree.find('target/path')
if pool_path_element is None:
msg = ('Missing "target/path" tag in the libvirt '
'storage pool "%(pool)s"'
'' % {'pool': self.STORAGE_POOL})
raise error.FishyError(msg)
image_name = '%s-%s.img' % (
os.path.basename(boot_image).replace('.', '-'),
domain.UUIDString())
image_path = os.path.join(
pool_path_element.text, image_name)
image_size = os.stat(boot_image).st_size
# Remove already existing volume
volumes_names = [v.name() for v in pool.listAllVolumes()]
if image_name in volumes_names:
volume = pool.storageVolLookupByName(image_name)
volume.delete()
# Create new volume
volume = pool.createXML(
self.STORAGE_VOLUME_XML % {
'name': image_name, 'path': image_path,
'size': image_size})
# Upload image to hypervisor
stream = conn.newStream()
volume.upload(stream, 0, image_size)
def read_file(stream, nbytes, fl):
return fl.read(nbytes)
stream.sendAll(read_file, open(boot_image, 'rb'))
stream.finish()
return image_path
def _default_controller(self, domain_tree):
os_element = domain_tree.find('os')
if os_element is not None:
type_element = os_element.find('type')
if type_element is not None:
arch = type_element.attrib.get('arch')
machine = type_element.attrib.get('machine')
if machine and 'q35' in machine:
# No IDE support for newer q35 machine types
return 'sata'
if arch and 'aarch64' in arch:
return 'scsi'
return 'ide'
def _add_boot_image(self, domain, domain_tree, device,
boot_image, write_protected):
identity = domain.UUIDString()
device_element = domain_tree.find('devices')
if device_element is None:
msg = ('Missing "devices" tag in the libvirt domain '
'"%(identity)s" configuration' % {'identity': identity})
raise error.FishyError(msg)
controller_type = self._default_controller(domain_tree)
with libvirt_open(self._uri) as conn:
image_path = self._upload_image(domain, conn, boot_image)
try:
lv_device = self.BOOT_DEVICE_MAP[device]
except KeyError:
raise error.BadRequest(
'Unknown device %s at %s' % (device, identity))
disk_elements = device_element.findall('disk')
for disk_element in disk_elements:
target_element = disk_element.find('target')
if target_element is None:
continue
elif target_element.attrib.get('bus') == 'scsi':
controller_type = 'scsi'
elif target_element.attrib.get('bus') == 'sata':
controller_type = 'sata'
if controller_type == 'ide':
tgt_dev, tgt_bus = self.DEVICE_TARGET_MAP[device]
elif lv_device == 'floppy':
tgt_dev, tgt_bus = ('fda', 'fdc')
else:
tgt_dev, tgt_bus = ('sdx', controller_type)
# Enumerate existing disks to find a free unit on the bus
free_units = {i for i in range(100)}
disk_elements = device_element.findall('disk')
for disk_element in disk_elements:
target_element = disk_element.find('target')
if target_element is None:
continue
bus_type = target_element.attrib.get('bus')
if bus_type != tgt_bus:
continue
address_element = disk_element.find('address')
if address_element is None:
continue
unit_num = address_element.attrib.get('unit')
if unit_num is None:
continue
if int(unit_num) in free_units:
free_units.remove(int(unit_num))
if not free_units:
msg = ('No free %(bus)s bus unit found in the libvirt domain '
'"%(identity)s" configuration' % {'identity': identity,
'bus': tgt_bus})
raise error.FishyError(msg)
# Add disk element pointing to the boot image
disk_element = ET.SubElement(device_element, 'disk')
disk_element.set('type', 'file')
disk_element.set('device', lv_device)
target_element = ET.SubElement(disk_element, 'target')
target_element.set('dev', tgt_dev)
target_element.set('bus', tgt_bus)
address_element = ET.SubElement(disk_element, 'address')
address_element.set('type', 'drive')
address_element.set('controller', '0')
address_element.set('bus', '0')
address_element.set('target', '0')
address_element.set('unit', '%s' % min(free_units))
driver_element = ET.SubElement(disk_element, 'driver')
driver_element.set('name', 'qemu')
driver_element.set('type', 'raw')
source_element = ET.SubElement(disk_element, 'source')
source_element.set('file', image_path)
if write_protected:
ET.SubElement(disk_element, 'readonly')
def _remove_boot_images(self, domain, domain_tree, device):
identity = domain.UUIDString()
try:
lv_device = self.BOOT_DEVICE_MAP[device]
except KeyError:
raise error.BadRequest(
'Unknown device %s at %s' % (device, identity))
device_element = domain_tree.find('devices')
if device_element is None:
msg = ('Missing "devices" tag in the libvirt domain '
'"%(identity)s" configuration' % {'identity': identity})
raise error.FishyError(msg)
# Remove all existing devices
disk_elements = device_element.findall('disk')
for disk_element in disk_elements:
dev_type = disk_element.attrib.get('device')
if dev_type == lv_device:
device_element.remove(disk_element)
def set_boot_image(self, identity, device, boot_image=None,
write_protected=True):
"""Set backend VM boot image
:param identity: libvirt domain name or ID
:param device: device type (from
`sushy_tools.emulator.constants`)
:param boot_image: path to the image file or `None` to remove
configured image entirely
:param write_protected: expose media as read-only or writable
:raises: `error.FishyError` if boot device can't be set
"""
domain = self._get_domain(identity)
domain_tree = ET.fromstring(self.get_xml_desc(domain))
self._remove_boot_images(domain, domain_tree, device)
boot_device = None
if boot_image:
self._add_boot_image(domain, domain_tree, device,
boot_image, write_protected)
boot_device = self.get_boot_device(identity)
with libvirt_open(self._uri) as conn:
xml = ET.tostring(domain_tree)
try:
conn.defineXML(xml.decode('utf-8'))
except Exception as e:
self._logger.error('Rejected libvirt domain XML is %s', xml)
msg = ('Error changing boot image at libvirt URI "%(uri)s": '
'%(error)s' % {'uri': self._uri, 'error': e})
raise error.FishyError(msg)
if device == boot_device:
self.set_boot_device(identity, boot_device)
def _find_device_by_path(self, vol_path):
"""Get device attributes using path
:param vol_path: path for the libvirt volume
:returns: a dict (or None) of the corresponding device attributes
"""
with libvirt_open(self._uri, readonly=True) as conn:
try:
vol = conn.storageVolLookupByPath(vol_path)
except libvirt.libvirtError as e:
msg = ('Could not find storage volume by path '
'"%(path)s" at libvirt URI "%(uri)s": '
'%(err)s' %
{'path': vol_path, 'uri': self._uri,
'err': e})
self._logger.debug(msg)
return
disk_device = {
'Name': vol.name(),
'CapacityBytes': vol.info()[1]
}
return disk_device
def _find_device_from_pool(self, pool_name, vol_name):
"""Get device attributes from pool
:param pool_name: libvirt pool name
:param vol_name: libvirt volume name
:returns: a dict (or None) of the corresponding device attributes
"""
with libvirt_open(self._uri, readonly=True) as conn:
try:
pool = conn.storagePoolLookupByName(pool_name)
except libvirt.libvirtError as e:
msg = ('Error finding Storage Pool by name "%(name)s" at'
'libvirt URI "%(uri)s": %(err)s' %
{'name': pool_name, 'uri': self._uri, 'err': e})
self._logger.debug(msg)
return
try:
vol = pool.storageVolLookupByName(vol_name)
except libvirt.libvirtError as e:
msg = ('Error finding Storage Volume by name "%(name)s" '
'in Pool '"%(pName)s"' at libvirt URI "%(uri)s"'
': %(err)s' %
{'name': vol_name, 'pName': pool_name,
'uri': self._uri, 'err': e})
self._logger.debug(msg)
return
disk_device = {
'Name': vol.name(),
'CapacityBytes': vol.info()[1]
}
return disk_device
def get_simple_storage_collection(self, identity):
"""Get a dict of simple storage controllers and their devices
Only those storage devices that are configured as a libvirt volume
via a pool and attached to the domain will reflect as a device.
Others are skipped.
:param identity: libvirt domain or ID
:returns: dict of simple storage controller dict with their attributes
"""
domain = self._get_domain(identity, readonly=True)
tree = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
simple_storage = defaultdict(lambda: defaultdict(DeviceList=list()))
for disk_element in tree.findall(".//disk/target[@bus]/.."):
source_element = disk_element.find('source')
if source_element is not None:
disk_type = disk_element.attrib['type']
ctl_type = disk_element.find('target').attrib['bus']
disk_device = None
if disk_type in ('file', 'block'):
if disk_type == 'file':
vol_path = source_element.attrib['file']
else:
vol_path = source_element.attrib['dev']
disk_device = self._find_device_by_path(vol_path)
elif disk_type == 'volume':
pool_name = source_element.attrib['pool']
vol_name = source_element.attrib['volume']
disk_device = self._find_device_from_pool(pool_name,
vol_name)
if disk_device is not None:
simple_storage[ctl_type]['Id'] = ctl_type
simple_storage[ctl_type]['Name'] = ctl_type
simple_storage[ctl_type]['DeviceList'].append(disk_device)
return simple_storage
def find_or_create_storage_volume(self, data):
"""Find/create volume based on existence in the virtualization backend
:param data: data about the volume in dict form with values for `Id`,
`Name`, `CapacityBytes`, `VolumeType`, `libvirtPoolName`
and `libvirtVolName`
:returns: Id of the volume if successfully found/created else None
"""
with libvirt_open(self._uri) as conn:
try:
poolName = data['libvirtPoolName']
except KeyError:
poolName = self.STORAGE_POOL
try:
pool = conn.storagePoolLookupByName(poolName)
except libvirt.libvirtError as ex:
msg = ('Error finding Storage Pool by name "%(name)s" at '
'libvirt URI "%(uri)s": %(err)s' %
{'name': poolName, 'uri': self._uri, 'err': ex})
self._logger.debug(msg)
return
try:
vol = pool.storageVolLookupByName(data['libvirtVolName'])
except libvirt.libvirtError:
msg = ('Creating storage volume with name: "%s"',
data['libvirtVolName'])
self._logger.debug(msg)
pool_tree = ET.fromstring(pool.XMLDesc())
# Find out path to the volume
pool_path_element = pool_tree.find('target/path')
if pool_path_element is None:
msg = ('Missing "target/path" tag in the libvirt '
'storage pool "%(pool)s"'
'' % {'pool': poolName})
self._logger.debug(msg)
return
vol_path = os.path.join(
pool_path_element.text, data['libvirtVolName'])
# Create a new volume
vol = pool.createXML(
self.STORAGE_VOLUME_XML % {
'name': data['libvirtVolName'], 'path': vol_path,
'size': data['CapacityBytes']})
if not vol:
msg = ('Error creating "%s" storage volume in "%s" pool',
data['libvirtVolName'], poolName)
self._logger.debug(msg)
return
return data['Id']
def get_http_boot_uri(self, identity):
"""Return the URI stored for the HttpBootUri.
:param identity: The libvirt identity. Unused, exists for internal
sushy-tools compatibility.
:returns: Stored URI value for HttpBootURI.
"""
return self._http_boot_uri
def set_http_boot_uri(self, uri):
"""Stores the Uri for HttpBootURI.
:param uri: String to return
:returns: None
"""
self._http_boot_uri = uri