diff --git a/qubes/ext/services.py b/qubes/ext/services.py index b09e2fda..52557d89 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -39,10 +39,28 @@ class ServicesExtension(qubes.ext.Extension): vm.untrusted_qdb.write('/qubes-service/{}'.format(service), 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:*') def on_domain_feature_set(self, vm, event, feature, value, oldvalue=None): '''Update /qubes-service/ QubesDB tree in runtime''' # 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(): return if not feature.startswith('service.'): @@ -61,8 +79,22 @@ class ServicesExtension(qubes.ext.Extension): if not feature.startswith('service.'): return service = feature[len('service.'):] + # this one is excluded from user control + if service == 'meminfo-writer': + return 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') def supported_services(self, vm, event, untrusted_features): '''Handle advertisement of supported services''' diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 04d70327..99fbb0ce 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -235,6 +235,7 @@ class TC_20_Services(qubes.tests.QubesTestCase): self.features = {} self.vm.configure_mock(**{ 'template': None, + 'maxmem': 1024, 'is_running.return_value': True, 'features.get.side_effect': self.features.get, '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.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), [ + ('write', ('/qubes-service/meminfo-writer', '1'), {}), ('write', ('/qubes-service/test1', '1'), {}), ('write', ('/qubes-service/test2', '0'), {}), ]) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index ce5c4077..42988cfa 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -44,6 +44,22 @@ class TestApp(object): def __init__(self): 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): # pylint: disable=too-few-public-methods @@ -80,6 +96,7 @@ class TestVM(object): for k, v in kwargs.items(): setattr(self, k, v) self.devices = {'pci': TestDeviceCollection()} + self.features = TestFeatures(self) def is_running(self): return self.running @@ -170,8 +187,6 @@ class TC_10_default(qubes.tests.QubesTestCase): self.assertEqual(default_getter(self.vm), 'template-kernel') 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), 'pvh') 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), '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): property_no_default = object() @@ -696,7 +753,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): expected = ''' test-inst-test 7db78950-c467-4863-94d1-af59806384ea - 500 + 400 400 2 @@ -797,6 +854,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): vm = self.get_vm(uuid=my_uuid) vm.netvm = None vm.virt_mode = 'hvm' + vm.features['qrexec'] = True with unittest.mock.patch('qubes.config.qubes_base_dir', '/tmp/qubes-test'): 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), lxml.etree.XML(expected)) + def test_600_libvirt_xml_pvh_no_membalance(self): + expected = ''' + test-inst-test + 7db78950-c467-4863-94d1-af59806384ea + 400 + 400 + 2 + + + + + + + + + pvh + /tmp/kernel/vmlinuz + /tmp/kernel/initramfs + root=/dev/mapper/dmroot ro nomodeset console=hvc0 rd_NO_PLYMOUTH rd.plymouth.enable=0 plymouth.enable=0 nopat + + + + + + + + + + + destroy + destroy + destroy + + + + + + + + + + + + + ''' + 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 = ''' + test-inst-test + 7db78950-c467-4863-94d1-af59806384ea + 400 + 400 + 2 + + + + + + + + + hvm + + hvmloader + + + + + + + + + + + + + + destroy + destroy + destroy + + + +
+ + + + + + + + + + ''' + 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): expected = ''' test-inst-test @@ -937,6 +1144,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): vm = self.get_vm(uuid=my_uuid) vm.netvm = netvm vm.virt_mode = 'hvm' + vm.features['qrexec'] = True with self.subTest('ipv4_only'): libvirt_xml = vm.create_config_file() self.assertXMLEqual(lxml.etree.XML(libvirt_xml), @@ -1000,6 +1208,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): '/qubes-iptables-error': '', '/qubes-iptables-header': iptables_header, '/qubes-service/qubes-update-check': '0', + '/qubes-service/meminfo-writer': '1', }) @unittest.mock.patch('qubes.utils.get_timezone') @@ -1060,6 +1269,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): '/qubes-iptables-error': '', '/qubes-iptables-header': iptables_header, '/qubes-service/qubes-update-check': '0', + '/qubes-service/meminfo-writer': '1', '/qubes-ip': '10.137.0.3', '/qubes-netmask': '255.255.255.255', '/qubes-gateway': '10.137.0.2', diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index e4cc14de..70a2c24d 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -69,13 +69,21 @@ def _setter_kernel(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 value = int(value) if value <= 0: raise ValueError('Value must be positive') 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): ''' Helper for setting default user ''' @@ -122,6 +130,25 @@ def _default_with_template(prop, default): 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): '''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.') maxmem = qubes.property('maxmem', type=int, - setter=_setter_positive_int, - default=_default_with_template('maxmem', (lambda self: - int(min(self.app.host.memory_total / 1024 / 2, 4000)))), + setter=_setter_non_negative_int, + default=_default_maxmem, doc='''Maximum amount of memory available for this VM (for the purpose - of the memory balancer). TemplateBasedVMs use its ' - 'template\'s value by default.''') + of the memory balancer). Set to 0 to disable memory balancing for + 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, 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, '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: # new qube, disable updates check if requested for new qubes # 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 + 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): # overhead of per-qube/per-vcpu Xen structures, # taken from OpenStack nova/virt/xenapi/driver.py diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml index a2c80287..8f3f7162 100644 --- a/templates/libvirt/xen.xml +++ b/templates/libvirt/xen.xml @@ -2,11 +2,12 @@ {% block basic %} {{ vm.name }} {{ vm.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) -%} {{ vm.memory }} - {% else %} + {% else -%} {{ vm.maxmem }} - {% endif %} + {% endif -%} {{ vm.memory }} {{ vm.vcpus }} {% endblock %}