Merge remote-tracking branch 'origin/pr/296'

* origin/pr/296:
  ext/block: prefer connecting cdrom as xvdd
  tests: extend mock objects in QubesVM tests
This commit is contained in:
Marek Marczykowski-Górecki 2019-11-26 23:56:33 +01:00
commit 09ab00cafd
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
4 changed files with 264 additions and 16 deletions

View File

@ -37,7 +37,9 @@ mode_re = re.compile(r"^[rw]$")
AVAILABLE_FRONTENDS = ['xvd'+c for c in
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):
@ -172,6 +174,9 @@ class BlockDeviceExtension(qubes.ext.Extension):
if not vm.is_running():
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())
for disk in xml_desc.findall('devices/disk'):
@ -187,7 +192,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
frontend_dev = target_node.get('dev')
if not frontend_dev:
continue
if frontend_dev in SYSTEM_DISKS:
if frontend_dev in system_disks:
continue
else:
continue
@ -217,7 +222,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
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
'''Find unused block frontend device node for <target dev=.../>
parameter'''
@ -227,6 +232,10 @@ class BlockDeviceExtension(qubes.ext.Extension):
parsed_xml = lxml.etree.fromstring(xml)
used = [target.get('dev', None) for target in
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:
if dev not in used:
return dev
@ -269,7 +278,8 @@ class BlockDeviceExtension(qubes.ext.Extension):
'it'.format(device.backend_domain.name))
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.app.env.get_template('libvirt/devices/block.xml').render(

View File

@ -25,6 +25,15 @@ import jinja2
import qubes.tests
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 type='xen' id='9'>
@ -66,13 +75,6 @@ domain_xml_template = '''
<backingStore/>
<target dev='xvdc' bus='xen'/>
</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'>
<mac address='00:16:3e:5e:6c:06'/>
@ -486,6 +488,46 @@ class TC_00_Block(qubes.tests.QubesTestCase):
'</disk>')
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):
back_vm = TestVM(name='sys-usb', qdb={
'/qubes-block-devices/sda': b'',

View File

@ -39,6 +39,7 @@ import shutil
import qubes
import qubes.exc
import qubes.config
import qubes.devices
import qubes.vm
import qubes.vm.qubesvm
@ -79,8 +80,10 @@ class TestDeviceCollection(object):
return self._list
class TestQubesDB(object):
def __init__(self):
def __init__(self, data=None):
self.data = {}
if data:
self.data = data
def write(self, path, value):
self.data[path] = value
@ -92,6 +95,12 @@ class TestQubesDB(object):
else:
self.data.pop(path, None)
def list(self, prefix):
return [key for key in self.data if key.startswith(prefix)]
def close(self):
pass
class TestVM(object):
# pylint: disable=too-few-public-methods
app = TestApp()
@ -269,10 +278,11 @@ class QubesVMTestsMixin(object):
pass
super(QubesVMTestsMixin, self).tearDown()
def get_vm(self, name='test', cls=qubes.vm.qubesvm.QubesVM, **kwargs):
vm = cls(self.app, None,
qid=kwargs.pop('qid', 1), name=qubes.tests.VMPREFIX + name,
**kwargs)
def get_vm(self, name='test', cls=qubes.vm.qubesvm.QubesVM, vm=None, **kwargs):
if not vm:
vm = cls(self.app, None,
qid=kwargs.pop('qid', 1), name=qubes.tests.VMPREFIX + name,
**kwargs)
self.app.domains[vm.qid] = vm
self.app.domains[vm.uuid] = vm
self.app.domains[vm.name] = vm
@ -1199,6 +1209,189 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
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):
expected = '''<domain type="xen">
<name>test-inst-test</name>

View File

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