diff --git a/qubes/ext/services.py b/qubes/ext/services.py index b09e2fda..cb9cd5f7 100644 --- a/qubes/ext/services.py +++ b/qubes/ext/services.py @@ -39,6 +39,10 @@ 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''' diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 6f59132f..094c80ca 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -224,6 +224,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, @@ -239,6 +240,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 308fd782..cb4df88d 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -665,7 +665,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): expected = ''' test-inst-test 7db78950-c467-4863-94d1-af59806384ea - 500 + 400 400 2 @@ -766,6 +766,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' @@ -906,6 +907,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), @@ -969,6 +971,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') @@ -1029,6 +1032,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 2b46c5b6..6b7abad9 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. @@ -448,12 +475,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, @@ -743,11 +770,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 @@ -1350,6 +1372,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 2829b0f9..3e6cf752 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 %}