Browse Source

Merge branch 'maxmem'

Marek Marczykowski-Górecki 5 years ago
parent
commit
3728230e3c
5 changed files with 312 additions and 17 deletions
  1. 32 0
      qubes/ext/services.py
  2. 2 0
      qubes/tests/ext.py
  3. 213 3
      qubes/tests/vm/qubesvm.py
  4. 61 11
      qubes/vm/qubesvm.py
  5. 4 3
      templates/libvirt/xen.xml

+ 32 - 0
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'''

+ 2 - 0
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'), {}),
         ])

+ 213 - 3
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 = '''<domain type="xen">
         <name>test-inst-test</name>
         <uuid>7db78950-c467-4863-94d1-af59806384ea</uuid>
-        <memory unit="MiB">500</memory>
+        <memory unit="MiB">400</memory>
         <currentMemory unit="MiB">400</currentMemory>
         <vcpu placement="static">2</vcpu>
         <cpu mode='host-passthrough'>
@@ -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 = '''<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):
         expected = '''<domain type="xen">
         <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.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',

+ 61 - 11
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

+ 4 - 3
templates/libvirt/xen.xml

@@ -2,11 +2,12 @@
     {% block basic %}
         <name>{{ vm.name }}</name>
         <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>
-        {% else %}
+        {% else -%}
             <memory unit="MiB">{{ vm.maxmem }}</memory>
-        {% endif %}
+        {% endif -%}
         <currentMemory unit="MiB">{{ vm.memory }}</currentMemory>
         <vcpu placement="static">{{ vm.vcpus }}</vcpu>
     {% endblock %}