ext/block: prefer connecting cdrom as xvdd

Only first 4 disks can be emulated as IDE disks by QEMU. Specifically,
CDROM must be one of those first 4 disks, otherwise it will be
ignored. This is especially important if one wants to boot the VM from
that CDROM.
Since xvdd normally is a kernel-related volume (boot image, modules) it
makes perfect sense to re-use it for CDROM. It is either set for kernel
volume (in which case, VM should boot from it and not the CDROM), or
(possibly bootable) CDROM.

This needs to be done in two places:
 - BlockExtension for dynamic attach
 - libvirt xen.xml - for before-boot attach

In theory the latter would be enough, but it would be quite confusing
that device will get different options depending on when it's attached
(in addition to whether the kernel is set - introduced here).

This all also means, xvdd not always is a "system disk". Adjust listing
connected disks accordingly.
This commit is contained in:
Marek Marczykowski-Górecki 2019-11-18 05:10:09 +01:00
parent 9bf0cce11e
commit 6c7af109e5
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
4 changed files with 249 additions and 11 deletions

View File

@ -37,7 +37,9 @@ mode_re = re.compile(r"^[rw]$")
AVAILABLE_FRONTENDS = ['xvd'+c for c in AVAILABLE_FRONTENDS = ['xvd'+c for c in
string.ascii_lowercase[8:]+string.ascii_lowercase[:8]] string.ascii_lowercase[8:]+string.ascii_lowercase[:8]]
SYSTEM_DISKS = ('xvda', 'xvdb', 'xvdc', 'xvdd') SYSTEM_DISKS = ('xvda', 'xvdb', 'xvdc')
# xvdd is considered system disk only if vm.kernel is set
SYSTEM_DISKS_DOM0_KERNEL = SYSTEM_DISKS + ('xvdd',)
class BlockDevice(qubes.devices.DeviceInfo): class BlockDevice(qubes.devices.DeviceInfo):
@ -172,6 +174,9 @@ class BlockDeviceExtension(qubes.ext.Extension):
if not vm.is_running(): if not vm.is_running():
return return
system_disks = SYSTEM_DISKS
if getattr(vm, 'kernel', None):
system_disks = SYSTEM_DISKS_DOM0_KERNEL
xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
for disk in xml_desc.findall('devices/disk'): for disk in xml_desc.findall('devices/disk'):
@ -187,7 +192,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
frontend_dev = target_node.get('dev') frontend_dev = target_node.get('dev')
if not frontend_dev: if not frontend_dev:
continue continue
if frontend_dev in SYSTEM_DISKS: if frontend_dev in system_disks:
continue continue
else: else:
continue continue
@ -217,7 +222,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
yield (BlockDevice(backend_domain, ident), options) yield (BlockDevice(backend_domain, ident), options)
def find_unused_frontend(self, vm): def find_unused_frontend(self, vm, devtype='disk'):
# pylint: disable=no-self-use # pylint: disable=no-self-use
'''Find unused block frontend device node for <target dev=.../> '''Find unused block frontend device node for <target dev=.../>
parameter''' parameter'''
@ -227,6 +232,10 @@ class BlockDeviceExtension(qubes.ext.Extension):
parsed_xml = lxml.etree.fromstring(xml) parsed_xml = lxml.etree.fromstring(xml)
used = [target.get('dev', None) for target in used = [target.get('dev', None) for target in
parsed_xml.xpath("//domain/devices/disk/target")] parsed_xml.xpath("//domain/devices/disk/target")]
if devtype == 'cdrom' and 'xvdd' not in used:
# prefer 'xvdd' for CDROM if available; only first 4 disks are
# emulated in HVM, which means only those are bootable
return 'xvdd'
for dev in AVAILABLE_FRONTENDS: for dev in AVAILABLE_FRONTENDS:
if dev not in used: if dev not in used:
return dev return dev
@ -269,7 +278,8 @@ class BlockDeviceExtension(qubes.ext.Extension):
'it'.format(device.backend_domain.name)) 'it'.format(device.backend_domain.name))
if 'frontend-dev' not in options: if 'frontend-dev' not in options:
options['frontend-dev'] = self.find_unused_frontend(vm) options['frontend-dev'] = self.find_unused_frontend(
vm, options.get('devtype', 'disk'))
vm.libvirt_domain.attachDevice( vm.libvirt_domain.attachDevice(
vm.app.env.get_template('libvirt/devices/block.xml').render( vm.app.env.get_template('libvirt/devices/block.xml').render(

View File

@ -25,6 +25,15 @@ import jinja2
import qubes.tests import qubes.tests
import qubes.ext.block import qubes.ext.block
modules_disk = '''
<disk type='block' device='disk'>
<driver name='phy'/>
<source dev='/var/lib/qubes/vm-kernels/4.4.55-11/modules.img'/>
<backingStore/>
<target dev='xvdd' bus='xen'/>
<readonly/>
</disk>
'''
domain_xml_template = ''' domain_xml_template = '''
<domain type='xen' id='9'> <domain type='xen' id='9'>
@ -66,13 +75,6 @@ domain_xml_template = '''
<backingStore/> <backingStore/>
<target dev='xvdc' bus='xen'/> <target dev='xvdc' bus='xen'/>
</disk> </disk>
<disk type='block' device='disk'>
<driver name='phy'/>
<source dev='/var/lib/qubes/vm-kernels/4.4.55-11/modules.img'/>
<backingStore/>
<target dev='xvdd' bus='xen'/>
<readonly/>
</disk>
{} {}
<interface type='ethernet'> <interface type='ethernet'>
<mac address='00:16:3e:5e:6c:06'/> <mac address='00:16:3e:5e:6c:06'/>
@ -486,6 +488,46 @@ class TC_00_Block(qubes.tests.QubesTestCase):
'</disk>') '</disk>')
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml) vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
def test_048_attach_cdrom_xvdi(self):
back_vm = TestVM(name='sys-usb', qdb={
'/qubes-block-devices/sda': b'',
'/qubes-block-devices/sda/desc': b'Test device',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'r',
})
vm = TestVM({}, domain_xml=domain_xml_template.format(modules_disk))
dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
self.ext.on_device_pre_attached_block(vm, '', dev, {'devtype': 'cdrom'})
device_xml = (
'<disk type="block" device="cdrom">\n'
' <driver name="phy" />\n'
' <source dev="/dev/sda" />\n'
' <target dev="xvdi" />\n'
' <readonly />\n'
' <backenddomain name="sys-usb" />\n'
'</disk>')
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
def test_048_attach_cdrom_xvdd(self):
back_vm = TestVM(name='sys-usb', qdb={
'/qubes-block-devices/sda': b'',
'/qubes-block-devices/sda/desc': b'Test device',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'r',
})
vm = TestVM({}, domain_xml=domain_xml_template.format(''))
dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
self.ext.on_device_pre_attached_block(vm, '', dev, {'devtype': 'cdrom'})
device_xml = (
'<disk type="block" device="cdrom">\n'
' <driver name="phy" />\n'
' <source dev="/dev/sda" />\n'
' <target dev="xvdd" />\n'
' <readonly />\n'
' <backenddomain name="sys-usb" />\n'
'</disk>')
vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
def test_050_detach(self): def test_050_detach(self):
back_vm = TestVM(name='sys-usb', qdb={ back_vm = TestVM(name='sys-usb', qdb={
'/qubes-block-devices/sda': b'', '/qubes-block-devices/sda': b'',

View File

@ -1209,6 +1209,189 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
self.assertXMLEqual(lxml.etree.XML(libvirt_xml), self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
lxml.etree.XML(expected)) lxml.etree.XML(expected))
def test_600_libvirt_xml_hvm_cdrom_boot(self):
expected = '''<domain type="xen">
<name>test-inst-test</name>
<uuid>7db78950-c467-4863-94d1-af59806384ea</uuid>
<memory unit="MiB">400</memory>
<currentMemory unit="MiB">400</currentMemory>
<vcpu placement="static">2</vcpu>
<cpu mode='host-passthrough'>
<!-- disable nested HVM -->
<feature name='vmx' policy='disable'/>
<feature name='svm' policy='disable'/>
<!-- disable SMAP inside VM, because of Linux bug -->
<feature name='smap' policy='disable'/>
</cpu>
<os>
<type arch="x86_64" machine="xenfv">hvm</type>
<!--
For the libxl backend libvirt switches between OVMF (UEFI)
and SeaBIOS based on the loader type. This has nothing to
do with the hvmloader binary.
-->
<loader type="rom">hvmloader</loader>
<boot dev="cdrom" />
<boot dev="hd" />
</os>
<features>
<pae/>
<acpi/>
<apic/>
<viridian/>
</features>
<clock offset="variable" adjustment="0" basis="localtime" />
<on_poweroff>destroy</on_poweroff>
<on_reboot>destroy</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<disk type="block" device="cdrom">
<driver name="phy" />
<source dev="/dev/sda" />
<!-- prefer xvdd for CDROM -->
<target dev="xvdd" />
<readonly/>
</disk>
<!-- server_ip is the address of stubdomain. It hosts it's own DNS server. -->
<emulator type="stubdom-linux" />
<input type="tablet" bus="usb"/>
<video>
<model type="vga"/>
</video>
<graphics type="qubes"/>
<console type="pty">
<target type="xen" port="0"/>
</console>
</devices>
</domain>
'''
my_uuid = '7db78950-c467-4863-94d1-af59806384ea'
qdb = {
'/qubes-block-devices/sda': b'',
'/qubes-block-devices/sda/desc': b'Test device',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'r',
}
test_qdb = TestQubesDB(qdb)
dom0 = qubes.vm.adminvm.AdminVM(self.app, None)
dom0._qdb_connection = test_qdb
self.get_vm('dom0', vm=dom0)
vm = self.get_vm(uuid=my_uuid)
vm.netvm = None
vm.virt_mode = 'hvm'
vm.kernel = None
dom0.events_enabled = True
self.app.vmm.offline_mode = False
dev = qubes.devices.DeviceAssignment(
dom0, 'sda',
{'devtype': 'cdrom', 'read-only': 'yes'}, persistent=True)
self.loop.run_until_complete(vm.devices['block'].attach(dev))
libvirt_xml = vm.create_config_file()
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
lxml.etree.XML(expected))
def test_600_libvirt_xml_hvm_cdrom_dom0_kernel_boot(self):
expected = '''<domain type="xen">
<name>test-inst-test</name>
<uuid>7db78950-c467-4863-94d1-af59806384ea</uuid>
<memory unit="MiB">400</memory>
<currentMemory unit="MiB">400</currentMemory>
<vcpu placement="static">2</vcpu>
<cpu mode='host-passthrough'>
<!-- disable nested HVM -->
<feature name='vmx' policy='disable'/>
<feature name='svm' policy='disable'/>
<!-- disable SMAP inside VM, because of Linux bug -->
<feature name='smap' policy='disable'/>
</cpu>
<os>
<type arch="x86_64" machine="xenfv">hvm</type>
<!--
For the libxl backend libvirt switches between OVMF (UEFI)
and SeaBIOS based on the loader type. This has nothing to
do with the hvmloader binary.
-->
<loader type="rom">hvmloader</loader>
<boot dev="cdrom" />
<boot dev="hd" />
<cmdline>root=/dev/mapper/dmroot ro nomodeset console=hvc0 rd_NO_PLYMOUTH rd.plymouth.enable=0 plymouth.enable=0 nopat</cmdline>
</os>
<features>
<pae/>
<acpi/>
<apic/>
<viridian/>
</features>
<clock offset="variable" adjustment="0" basis="localtime" />
<on_poweroff>destroy</on_poweroff>
<on_reboot>destroy</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<disk type="block" device="disk">
<driver name="phy" />
<source dev="/tmp/kernel/modules.img" />
<target dev="xvdd" />
<backenddomain name="dom0" />
</disk>
<disk type="block" device="cdrom">
<driver name="phy" />
<source dev="/dev/sda" />
<target dev="xvdi" />
<readonly/>
</disk>
<!-- server_ip is the address of stubdomain. It hosts it's own DNS server. -->
<emulator type="stubdom-linux" />
<input type="tablet" bus="usb"/>
<video>
<model type="vga"/>
</video>
<graphics type="qubes"/>
<console type="pty">
<target type="xen" port="0"/>
</console>
</devices>
</domain>
'''
qdb = {
'/qubes-block-devices/sda': b'',
'/qubes-block-devices/sda/desc': b'Test device',
'/qubes-block-devices/sda/size': b'1024000',
'/qubes-block-devices/sda/mode': b'r',
}
test_qdb = TestQubesDB(qdb)
dom0 = qubes.vm.adminvm.AdminVM(self.app, None)
dom0._qdb_connection = test_qdb
my_uuid = '7db78950-c467-4863-94d1-af59806384ea'
vm = self.get_vm(uuid=my_uuid)
vm.netvm = None
vm.virt_mode = 'hvm'
with unittest.mock.patch('qubes.config.qubes_base_dir',
'/tmp/qubes-test'):
kernel_dir = '/tmp/qubes-test/vm-kernels/dummy'
os.makedirs(kernel_dir, exist_ok=True)
open(os.path.join(kernel_dir, 'vmlinuz'), 'w').close()
open(os.path.join(kernel_dir, 'initramfs'), 'w').close()
self.addCleanup(shutil.rmtree, '/tmp/qubes-test')
vm.kernel = 'dummy'
# tests for storage are later
vm.volumes['kernel'] = unittest.mock.Mock(**{
'kernels_dir': '/tmp/kernel',
'block_device.return_value.domain': 'dom0',
'block_device.return_value.script': None,
'block_device.return_value.path': '/tmp/kernel/modules.img',
'block_device.return_value.devtype': 'disk',
'block_device.return_value.name': 'kernel',
})
dom0.events_enabled = True
self.app.vmm.offline_mode = False
dev = qubes.devices.DeviceAssignment(
dom0, 'sda',
{'devtype': 'cdrom', 'read-only': 'yes'}, persistent=True)
self.loop.run_until_complete(vm.devices['block'].attach(dev))
libvirt_xml = vm.create_config_file()
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
lxml.etree.XML(expected))
def test_610_libvirt_xml_network(self): def test_610_libvirt_xml_network(self):
expected = '''<domain type="xen"> expected = '''<domain type="xen">
<name>test-inst-test</name> <name>test-inst-test</name>

View File

@ -3,6 +3,9 @@
<source dev="{{ device.device_node }}" /> <source dev="{{ device.device_node }}" />
{%- if 'frontend-dev' in options %} {%- if 'frontend-dev' in options %}
<target dev="{{ options.get('frontend-dev') }}" /> <target dev="{{ options.get('frontend-dev') }}" />
{%- elif options.get('devtype', 'disk') == 'cdrom' and not vm.kernel %}
<!-- prefer xvdd for CDROM -->
<target dev="xvdd" />
{%- else %} {%- else %}
<target dev="xvd{{dd[i]}}" /> <target dev="xvd{{dd[i]}}" />
{% set i = i + 1 %} {% set i = i + 1 %}