Merge branch 'maxmem'

This commit is contained in:
Marek Marczykowski-Górecki 2018-12-09 18:38:21 +01:00
commit 3728230e3c
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 312 additions and 17 deletions

View File

@ -39,10 +39,28 @@ class ServicesExtension(qubes.ext.Extension):
vm.untrusted_qdb.write('/qubes-service/{}'.format(service), vm.untrusted_qdb.write('/qubes-service/{}'.format(service),
str(int(bool(value)))) str(int(bool(value))))
# always set meminfo-writer according to maxmem
vm.untrusted_qdb.write('/qubes-service/meminfo-writer',
'1' if vm.maxmem > 0 else '0')
@qubes.ext.handler('domain-feature-set:*') @qubes.ext.handler('domain-feature-set:*')
def on_domain_feature_set(self, vm, event, feature, value, oldvalue=None): def on_domain_feature_set(self, vm, event, feature, value, oldvalue=None):
'''Update /qubes-service/ QubesDB tree in runtime''' '''Update /qubes-service/ QubesDB tree in runtime'''
# pylint: disable=unused-argument # pylint: disable=unused-argument
# TODO: remove this compatibility hack in Qubes 4.1
if feature == 'service.meminfo-writer':
# if someone try to enable meminfo-writer ...
if value:
# ... reset maxmem to default
vm.maxmem = qubes.property.DEFAULT
else:
# otherwise, set to 0
vm.maxmem = 0
# in any case, remove the entry, as it does not indicate memory
# balancing state anymore
del vm.features['service.meminfo-writer']
if not vm.is_running(): if not vm.is_running():
return return
if not feature.startswith('service.'): if not feature.startswith('service.'):
@ -61,8 +79,22 @@ class ServicesExtension(qubes.ext.Extension):
if not feature.startswith('service.'): if not feature.startswith('service.'):
return return
service = feature[len('service.'):] service = feature[len('service.'):]
# this one is excluded from user control
if service == 'meminfo-writer':
return
vm.untrusted_qdb.rm('/qubes-service/{}'.format(service)) vm.untrusted_qdb.rm('/qubes-service/{}'.format(service))
@qubes.ext.handler('domain-load')
def on_domain_load(self, vm, event):
'''Migrate meminfo-writer service into maxmem'''
# pylint: disable=no-self-use,unused-argument
if 'service.meminfo-writer' in vm.features:
# if was set to false, force maxmem=0
# otherwise, simply ignore as the default is fine
if not vm.features['service.meminfo-writer']:
vm.maxmem = 0
del vm.features['service.meminfo-writer']
@qubes.ext.handler('features-request') @qubes.ext.handler('features-request')
def supported_services(self, vm, event, untrusted_features): def supported_services(self, vm, event, untrusted_features):
'''Handle advertisement of supported services''' '''Handle advertisement of supported services'''

View File

@ -235,6 +235,7 @@ class TC_20_Services(qubes.tests.QubesTestCase):
self.features = {} self.features = {}
self.vm.configure_mock(**{ self.vm.configure_mock(**{
'template': None, 'template': None,
'maxmem': 1024,
'is_running.return_value': True, 'is_running.return_value': True,
'features.get.side_effect': self.features.get, 'features.get.side_effect': self.features.get,
'features.items.side_effect': self.features.items, 'features.items.side_effect': self.features.items,
@ -250,6 +251,7 @@ class TC_20_Services(qubes.tests.QubesTestCase):
self.ext.on_domain_qdb_create(self.vm, 'domain-qdb-create') self.ext.on_domain_qdb_create(self.vm, 'domain-qdb-create')
self.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), [ self.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), [
('write', ('/qubes-service/meminfo-writer', '1'), {}),
('write', ('/qubes-service/test1', '1'), {}), ('write', ('/qubes-service/test1', '1'), {}),
('write', ('/qubes-service/test2', '0'), {}), ('write', ('/qubes-service/test2', '0'), {}),
]) ])

View File

@ -44,6 +44,22 @@ class TestApp(object):
def __init__(self): def __init__(self):
self.domains = {} self.domains = {}
self.host = unittest.mock.Mock()
self.host.memory_total = 4096 * 1024
class TestFeatures(dict):
def __init__(self, vm, **kwargs) -> None:
self.vm = vm
super().__init__(**kwargs)
def check_with_template(self, feature, default):
vm = self.vm
while vm is not None:
try:
return vm.features[feature]
except KeyError:
vm = getattr(vm, 'template', None)
return default
class TestProp(object): class TestProp(object):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -80,6 +96,7 @@ class TestVM(object):
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(self, k, v) setattr(self, k, v)
self.devices = {'pci': TestDeviceCollection()} self.devices = {'pci': TestDeviceCollection()}
self.features = TestFeatures(self)
def is_running(self): def is_running(self):
return self.running return self.running
@ -170,8 +187,6 @@ class TC_10_default(qubes.tests.QubesTestCase):
self.assertEqual(default_getter(self.vm), 'template-kernel') self.assertEqual(default_getter(self.vm), 'template-kernel')
def test_010_default_virt_mode(self): def test_010_default_virt_mode(self):
default_getter = qubes.vm.qubesvm._default_with_template('kernel',
lambda x: x.app.default_kernel)
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm), self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'pvh') 'pvh')
self.vm.template = unittest.mock.Mock() self.vm.template = unittest.mock.Mock()
@ -185,6 +200,48 @@ class TC_10_default(qubes.tests.QubesTestCase):
self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm), self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm),
'hvm') 'hvm')
def test_020_default_maxmem(self):
default_maxmem = 2048
self.vm.is_memory_balancing_possible = \
lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible(
self.vm)
self.vm.virt_mode = 'pvh'
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm),
default_maxmem)
self.vm.virt_mode = 'hvm'
# HVM without qubes tools
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 0)
# just 'qrexec' feature
self.vm.features['qrexec'] = True
print(self.vm.features.check_with_template('qrexec', False))
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm),
default_maxmem)
# some supported-service.*, but not meminfo-writer
self.vm.features['supported-service.qubes-firewall'] = True
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 0)
# then add meminfo-writer
self.vm.features['supported-service.meminfo-writer'] = True
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm),
default_maxmem)
def test_021_default_maxmem_with_pcidevs(self):
self.vm.is_memory_balancing_possible = \
lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible(
self.vm)
self.vm.devices['pci'].persistent().append('00_00.0')
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 0)
def test_022_default_maxmem_linux(self):
self.vm.is_memory_balancing_possible = \
lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible(
self.vm)
self.vm.virt_mode = 'pvh'
self.vm.memory = 400
self.vm.features['os'] = 'Linux'
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 2048)
self.vm.memory = 100
self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 1000)
class QubesVMTestsMixin(object): class QubesVMTestsMixin(object):
property_no_default = object() property_no_default = object()
@ -696,7 +753,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
expected = '''<domain type="xen"> expected = '''<domain type="xen">
<name>test-inst-test</name> <name>test-inst-test</name>
<uuid>7db78950-c467-4863-94d1-af59806384ea</uuid> <uuid>7db78950-c467-4863-94d1-af59806384ea</uuid>
<memory unit="MiB">500</memory> <memory unit="MiB">400</memory>
<currentMemory unit="MiB">400</currentMemory> <currentMemory unit="MiB">400</currentMemory>
<vcpu placement="static">2</vcpu> <vcpu placement="static">2</vcpu>
<cpu mode='host-passthrough'> <cpu mode='host-passthrough'>
@ -797,6 +854,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
vm = self.get_vm(uuid=my_uuid) vm = self.get_vm(uuid=my_uuid)
vm.netvm = None vm.netvm = None
vm.virt_mode = 'hvm' vm.virt_mode = 'hvm'
vm.features['qrexec'] = True
with unittest.mock.patch('qubes.config.qubes_base_dir', with unittest.mock.patch('qubes.config.qubes_base_dir',
'/tmp/qubes-test'): '/tmp/qubes-test'):
kernel_dir = '/tmp/qubes-test/vm-kernels/dummy' kernel_dir = '/tmp/qubes-test/vm-kernels/dummy'
@ -879,6 +937,155 @@ 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_pvh_no_membalance(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">pvh</type>
<kernel>/tmp/kernel/vmlinuz</kernel>
<initrd>/tmp/kernel/initramfs</initrd>
<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='utc' adjustment='reset'>
<timer name="tsc" mode="native"/>
</clock>
<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>
<console type="pty">
<target type="xen" port="0"/>
</console>
</devices>
</domain>
'''
my_uuid = '7db78950-c467-4863-94d1-af59806384ea'
vm = self.get_vm(uuid=my_uuid)
vm.netvm = None
vm.virt_mode = 'pvh'
vm.maxmem = 0
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',
})
libvirt_xml = vm.create_config_file()
self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
lxml.etree.XML(expected))
def test_600_libvirt_xml_hvm_pcidev(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/>
<xen>
<e820_host state="on"/>
</xen>
</features>
<clock offset="variable" adjustment="0" basis="localtime" />
<on_poweroff>destroy</on_poweroff>
<on_reboot>destroy</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<hostdev type="pci" managed="yes">
<source>
<address
bus="0x00"
slot="0x00"
function="0x0" />
</source>
</hostdev>
<!-- 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"/>
</devices>
</domain>
'''
my_uuid = '7db78950-c467-4863-94d1-af59806384ea'
# required for PCI devices listing
self.app.vmm.offline_mode = False
vm = self.get_vm(uuid=my_uuid)
vm.netvm = None
vm.virt_mode = 'hvm'
vm.kernel = None
# even with meminfo-writer enabled, should have memory==maxmem
vm.features['service.meminfo-writer'] = True
assignment = qubes.devices.DeviceAssignment(
vm, # this is violation of API, but for PCI the argument
# is unused
'00_00.0',
bus='pci',
persistent=True)
vm.devices['pci']._set.add(
assignment)
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>
@ -937,6 +1144,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
vm = self.get_vm(uuid=my_uuid) vm = self.get_vm(uuid=my_uuid)
vm.netvm = netvm vm.netvm = netvm
vm.virt_mode = 'hvm' vm.virt_mode = 'hvm'
vm.features['qrexec'] = True
with self.subTest('ipv4_only'): with self.subTest('ipv4_only'):
libvirt_xml = vm.create_config_file() libvirt_xml = vm.create_config_file()
self.assertXMLEqual(lxml.etree.XML(libvirt_xml), self.assertXMLEqual(lxml.etree.XML(libvirt_xml),
@ -1000,6 +1208,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
'/qubes-iptables-error': '', '/qubes-iptables-error': '',
'/qubes-iptables-header': iptables_header, '/qubes-iptables-header': iptables_header,
'/qubes-service/qubes-update-check': '0', '/qubes-service/qubes-update-check': '0',
'/qubes-service/meminfo-writer': '1',
}) })
@unittest.mock.patch('qubes.utils.get_timezone') @unittest.mock.patch('qubes.utils.get_timezone')
@ -1060,6 +1269,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
'/qubes-iptables-error': '', '/qubes-iptables-error': '',
'/qubes-iptables-header': iptables_header, '/qubes-iptables-header': iptables_header,
'/qubes-service/qubes-update-check': '0', '/qubes-service/qubes-update-check': '0',
'/qubes-service/meminfo-writer': '1',
'/qubes-ip': '10.137.0.3', '/qubes-ip': '10.137.0.3',
'/qubes-netmask': '255.255.255.255', '/qubes-netmask': '255.255.255.255',
'/qubes-gateway': '10.137.0.2', '/qubes-gateway': '10.137.0.2',

View File

@ -69,13 +69,21 @@ def _setter_kernel(self, prop, value):
def _setter_positive_int(self, prop, value): def _setter_positive_int(self, prop, value):
''' Helper for setting a positive int. Checks that the int is >= 0 ''' ''' Helper for setting a positive int. Checks that the int is > 0 '''
# pylint: disable=unused-argument # pylint: disable=unused-argument
value = int(value) value = int(value)
if value <= 0: if value <= 0:
raise ValueError('Value must be positive') raise ValueError('Value must be positive')
return value return value
def _setter_non_negative_int(self, prop, value):
''' Helper for setting a positive int. Checks that the int is >= 0 '''
# pylint: disable=unused-argument
value = int(value)
if value < 0:
raise ValueError('Value must be positive or zero')
return value
def _setter_default_user(self, prop, value): def _setter_default_user(self, prop, value):
''' Helper for setting default user ''' ''' Helper for setting default user '''
@ -122,6 +130,25 @@ def _default_with_template(prop, default):
return _func return _func
def _default_maxmem(self):
# first check for any reason to _not_ enable qmemman
if not self.is_memory_balancing_possible():
return 0
# Linux specific cap: max memory can't scale beyond 10.79*init_mem
# see https://groups.google.com/forum/#!topic/qubes-devel/VRqkFj1IOtA
if self.features.get('os', None) == 'Linux':
default_maxmem = self.memory * 10
else:
default_maxmem = 4000
# don't use default larger than half of physical ram
default_maxmem = min(default_maxmem,
int(self.app.host.memory_total / 1024 / 2))
return _default_with_template('maxmem', default_maxmem)(self)
class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
'''Base functionality of Qubes VM shared between all VMs. '''Base functionality of Qubes VM shared between all VMs.
@ -457,12 +484,12 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
'template\'s value by default.') 'template\'s value by default.')
maxmem = qubes.property('maxmem', type=int, maxmem = qubes.property('maxmem', type=int,
setter=_setter_positive_int, setter=_setter_non_negative_int,
default=_default_with_template('maxmem', (lambda self: default=_default_maxmem,
int(min(self.app.host.memory_total / 1024 / 2, 4000)))),
doc='''Maximum amount of memory available for this VM (for the purpose doc='''Maximum amount of memory available for this VM (for the purpose
of the memory balancer). TemplateBasedVMs use its ' of the memory balancer). Set to 0 to disable memory balancing for
'template\'s value by default.''') this qube. TemplateBasedVMs use its template\'s value by default
(unless memory balancing not supported for this qube).''')
stubdom_mem = qubes.property('stubdom_mem', type=int, stubdom_mem = qubes.property('stubdom_mem', type=int,
setter=_setter_positive_int, setter=_setter_positive_int,
@ -759,11 +786,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
assert hasattr(self, 'qid') assert hasattr(self, 'qid')
assert hasattr(self, 'name') assert hasattr(self, 'name')
# Linux specific cap: max memory can't scale beyond 10.79*init_mem
# see https://groups.google.com/forum/#!topic/qubes-devel/VRqkFj1IOtA
if self.maxmem > self.memory * 10:
self.maxmem = self.memory * 10
if xml is None: if xml is None:
# new qube, disable updates check if requested for new qubes # new qube, disable updates check if requested for new qubes
# SEE: 1637 when features are done, migrate to plugin # SEE: 1637 when features are done, migrate to plugin
@ -1367,6 +1389,34 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
return stdouterr return stdouterr
def is_memory_balancing_possible(self):
'''Check if memory balancing can be enabled.
Reasons to not enable it:
- have PCI devices
- balloon driver not present
We don't have reliable way to detect the second point, but good
heuristic is HVM virt_mode (PV and PVH require OS support and it does
include balloon driver) and lack of qrexec/meminfo-writer service
support (no qubes tools installed).
'''
if list(self.devices['pci'].persistent()):
return False
if self.virt_mode == 'hvm':
# if VM announce any supported service
features_set = set(self.features)
template = getattr(self, 'template', None)
while template is not None:
features_set.update(template.features)
template = getattr(template, 'template', None)
supported_services = any(f.startswith('supported-service.')
for f in features_set)
if (not self.features.check_with_template('qrexec', False) or
(supported_services and not self.features.check_with_template(
'supported-service.meminfo-writer', False))):
return False
return True
def request_memory(self, mem_required=None): def request_memory(self, mem_required=None):
# overhead of per-qube/per-vcpu Xen structures, # overhead of per-qube/per-vcpu Xen structures,
# taken from OpenStack nova/virt/xenapi/driver.py # taken from OpenStack nova/virt/xenapi/driver.py

View File

@ -2,11 +2,12 @@
{% block basic %} {% block basic %}
<name>{{ vm.name }}</name> <name>{{ vm.name }}</name>
<uuid>{{ vm.uuid }}</uuid> <uuid>{{ vm.uuid }}</uuid>
{% if vm.virt_mode == 'hvm' and vm.devices['pci'].persistent() | list %} {% if ((vm.virt_mode == 'hvm' and vm.devices['pci'].persistent() | list)
or vm.maxmem == 0) -%}
<memory unit="MiB">{{ vm.memory }}</memory> <memory unit="MiB">{{ vm.memory }}</memory>
{% else %} {% else -%}
<memory unit="MiB">{{ vm.maxmem }}</memory> <memory unit="MiB">{{ vm.maxmem }}</memory>
{% endif %} {% endif -%}
<currentMemory unit="MiB">{{ vm.memory }}</currentMemory> <currentMemory unit="MiB">{{ vm.memory }}</currentMemory>
<vcpu placement="static">{{ vm.vcpus }}</vcpu> <vcpu placement="static">{{ vm.vcpus }}</vcpu>
{% endblock %} {% endblock %}