Ver código fonte

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

* origin/pr/295:
  tests: fix tag name in audiovm test
  tests: ensure notin while setting Audio/Gui VM
  gui: add checks for changing/removing guivm
  audio: add checks for changing/removing audiovm
  audio/gui: use simply vm.tags instead of list()
  tests: fix tests for gui/audio vm
  Make pylint happy
  gui/audio: fixes from Marek's comments
  Allow AudioVM to be ran after any attached qubes
  Allow GuiVM to be ran after any attached qubes
  xid: ensure vm is not running
  tests: fix missing default audiovm and guivm tags
  gui, audio: better handling of start/stop guivm/audiovm
  gui, audio: ensure guivm and audiovm tag are set
  Support for AudioVM
Marek Marczykowski-Górecki 4 anos atrás
pai
commit
16bdeea2c0

+ 6 - 0
qubes/app.py

@@ -743,6 +743,12 @@ class Qubes(qubes.PropertyHolder):
         default=lambda app: app.domains['dom0'], allow_none=True,
         doc='Default GuiVM for VMs.')
 
+    default_audiovm = qubes.VMProperty(
+        'default_audiovm',
+        load_stage=3,
+        default=lambda app: app.domains['dom0'], allow_none=True,
+        doc='Default AudioVM for VMs.')
+
     default_netvm = qubes.VMProperty(
         'default_netvm',
         load_stage=3,

+ 93 - 0
qubes/ext/audio.py

@@ -0,0 +1,93 @@
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2019 Frédéric Pierret <frederic.pierret@qubes-os.org>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, see <https://www.gnu.org/licenses/>.
+#
+
+import asyncio
+
+import qubes.config
+import qubes.ext
+
+
+class AUDIO(qubes.ext.Extension):
+    # pylint: disable=unused-argument,no-self-use
+    @staticmethod
+    def attached_vms(vm):
+        for domain in vm.app.domains:
+            if getattr(domain, 'audiovm', None) and domain.audiovm == vm:
+                yield domain
+
+    @qubes.ext.handler('domain-pre-shutdown')
+    @asyncio.coroutine
+    def on_domain_pre_shutdown(self, vm, event, **kwargs):
+        attached_vms = [domain for domain in self.attached_vms(vm) if
+                        domain.is_running()]
+        if attached_vms and not kwargs.get('force', False):
+            raise qubes.exc.QubesVMError(
+                self, 'There are running VMs using this VM as AudioVM: '
+                      '{}'.format(', '.join(vm.name for vm in attached_vms)))
+
+    @qubes.ext.handler('domain-init', 'domain-load')
+    def on_domain_init_load(self, vm, event):
+        if getattr(vm, 'audiovm', None):
+            if 'audiovm-' + vm.audiovm.name not in vm.tags:
+                self.on_property_set(vm, event, name='audiovm',
+                                     newvalue=vm.audiovm)
+
+    # property-del <=> property-reset-to-default
+    @qubes.ext.handler('property-del:audiovm')
+    def on_property_del(self, subject, event, name, oldvalue=None):
+        newvalue = getattr(subject, 'audiovm', None)
+        self.on_property_set(subject, event, name, newvalue, oldvalue)
+
+    @qubes.ext.handler('property-set:audiovm')
+    def on_property_set(self, subject, event, name, newvalue, oldvalue=None):
+        # Clean other 'audiovm-XXX' tags.
+        # pulseaudio agent (module-vchan-sink) can connect to only one domain
+        tags_list = list(subject.tags)
+        for tag in tags_list:
+            if tag.startswith('audiovm-'):
+                subject.tags.remove(tag)
+
+        if newvalue:
+            audiovm = 'audiovm-' + newvalue.name
+            subject.tags.add(audiovm)
+
+    @qubes.ext.handler('domain-qdb-create')
+    def on_domain_qdb_create(self, vm, event):
+        # Add AudioVM Xen ID for gui-agent
+        if getattr(vm, 'audiovm', None):
+            if vm != vm.audiovm and vm.audiovm.is_running():
+                vm.untrusted_qdb.write('/qubes-audio-domain-xid',
+                                       str(vm.audiovm.xid))
+
+    @qubes.ext.handler('property-set:default_audiovm', system=True)
+    def on_property_set_default_audiovm(self, app, event, name, newvalue,
+                                        oldvalue=None):
+        for vm in app.domains:
+            if hasattr(vm, 'audiovm') and vm.property_is_default('audiovm'):
+                vm.fire_event('property-set:audiovm',
+                              name='audiovm', newvalue=newvalue,
+                              oldvalue=oldvalue)
+
+    @qubes.ext.handler('domain-start')
+    def on_domain_start(self, vm, event, **kwargs):
+        attached_vms = [domain for domain in self.attached_vms(vm) if
+                        domain.is_running()]
+        for attached_vm in attached_vms:
+            attached_vm.untrusted_qdb.write('/qubes-audio-domain-xid',
+                                            str(vm.xid))

+ 32 - 8
qubes/ext/gui.py

@@ -26,8 +26,22 @@ import qubes.ext
 
 
 class GUI(qubes.ext.Extension):
-    # pylint: disable=too-few-public-methods
-    # TODO put this somewhere...
+    # pylint: disable=too-few-public-methods,unused-argument,no-self-use
+    @staticmethod
+    def attached_vms(vm):
+        for domain in vm.app.domains:
+            if getattr(domain, 'guivm', None) and domain.guivm == vm:
+                yield domain
+
+    @qubes.ext.handler('domain-pre-shutdown')
+    def on_domain_pre_shutdown(self, vm, event, **kwargs):
+        attached_vms = [domain for domain in self.attached_vms(vm) if
+                        domain.is_running()]
+        if attached_vms and not kwargs.get('force', False):
+            raise qubes.exc.QubesVMError(
+                self, 'There are running VMs using this VM as GuiVM: '
+                      '{}'.format(', '.join(vm.name for vm in attached_vms)))
+
     @staticmethod
     def send_gui_mode(vm):
         vm.run_service('qubes.SetGuiMode',
@@ -35,6 +49,12 @@ class GUI(qubes.ext.Extension):
                               if vm.features.get('gui-seamless', False)
                               else 'FULLSCREEN'))
 
+    @qubes.ext.handler('domain-init', 'domain-load')
+    def on_domain_init_load(self, vm, event):
+        if getattr(vm, 'guivm', None):
+            if 'guivm-' + vm.guivm.name not in vm.tags:
+                self.on_property_set(vm, event, name='guivm', newvalue=vm.guivm)
+
     # property-del <=> property-reset-to-default
     @qubes.ext.handler('property-del:guivm')
     def on_property_del(self, subject, event, name, oldvalue=None):
@@ -43,13 +63,11 @@ class GUI(qubes.ext.Extension):
 
     @qubes.ext.handler('property-set:guivm')
     def on_property_set(self, subject, event, name, newvalue, oldvalue=None):
-        # pylint: disable=unused-argument,no-self-use
-
         # Clean other 'guivm-XXX' tags.
         # gui-daemon can connect to only one domain
         tags_list = list(subject.tags)
         for tag in tags_list:
-            if 'guivm-' in tag:
+            if tag.startswith('guivm-'):
                 subject.tags.remove(tag)
 
         if newvalue:
@@ -58,7 +76,6 @@ class GUI(qubes.ext.Extension):
 
     @qubes.ext.handler('domain-qdb-create')
     def on_domain_qdb_create(self, vm, event):
-        # pylint: disable=unused-argument,no-self-use
         for feature in ('gui-videoram-overhead', 'gui-videoram-min'):
             try:
                 vm.untrusted_qdb.write(
@@ -70,7 +87,7 @@ class GUI(qubes.ext.Extension):
 
         # Add GuiVM Xen ID for gui-daemon
         if getattr(vm, 'guivm', None):
-            if vm != vm.guivm:
+            if vm != vm.guivm and vm.guivm.is_running():
                 vm.untrusted_qdb.write('/qubes-gui-domain-xid',
                                        str(vm.guivm.xid))
 
@@ -98,9 +115,16 @@ class GUI(qubes.ext.Extension):
     @qubes.ext.handler('property-set:default_guivm', system=True)
     def on_property_set_default_guivm(self, app, event, name, newvalue,
                                       oldvalue=None):
-        # pylint: disable=unused-argument,no-self-use
         for vm in app.domains:
             if hasattr(vm, 'guivm') and vm.property_is_default('guivm'):
                 vm.fire_event('property-set:guivm',
                               name='guivm', newvalue=newvalue,
                               oldvalue=oldvalue)
+
+    @qubes.ext.handler('domain-start')
+    def on_domain_start(self, vm, event, **kwargs):
+        attached_vms = [domain for domain in self.attached_vms(vm) if
+                        domain.is_running()]
+        for attached_vm in attached_vms:
+            attached_vm.untrusted_qdb.write('/qubes-gui-domain-xid',
+                                            str(vm.xid))

+ 1 - 1
qubes/tests/api_admin.py

@@ -1951,7 +1951,7 @@ netvm default=True type=vm \n'''
         self.vm.tags.add('tag1')
         self.vm.tags.add('tag2')
         value = self.call_mgmt_func(b'admin.vm.tag.List', b'test-vm1')
-        self.assertEqual(value, 'tag1\ntag2\n')
+        self.assertEqual(value, 'audiovm-dom0\nguivm-dom0\ntag1\ntag2\n')
         self.assertFalse(self.app.save.called)
 
     def test_540_tag_get(self):

+ 72 - 2
qubes/tests/app.py

@@ -596,18 +596,88 @@ class TC_90_Qubes(qubes.tests.QubesTestCase):
         holder = MyTestHolder(None)
         guivm = self.app.add_new_vm('AppVM', name='sys-gui', guivm='dom0',
                                     template=self.template, label='red')
+        vncvm = self.app.add_new_vm('AppVM', name='sys-vnc', guivm='dom0',
+                                    template=self.template, label='red')
         appvm = self.app.add_new_vm('AppVM', name='test-vm', guivm='dom0',
                                     template=self.template, label='red')
         holder.guivm = 'sys-gui'
         self.assertEqual(holder.guivm, 'sys-gui')
-        self.assertFalse(appvm.property_is_default('guivm'))
-        appvm.guivm = guivm
         self.assertEventFired(holder, 'property-set:guivm',
                               kwargs={'name': 'guivm',
                                       'newvalue': 'sys-gui'})
 
+        # Set GuiVM
+        self.assertFalse(appvm.property_is_default('guivm'))
+        appvm.guivm = guivm
         self.assertIn('guivm-sys-gui', appvm.tags)
 
+        # Change GuiVM
+        appvm.guivm = vncvm
+        self.assertIn('guivm-sys-vnc', appvm.tags)
+        self.assertNotIn('guivm-sys-gui', appvm.tags)
+
+        # Empty GuiVM
+        del appvm.guivm
+        self.assertNotIn('guivm-sys-vnc', appvm.tags)
+        self.assertNotIn('guivm-sys-gui', appvm.tags)
+        self.assertNotIn('guivm-', appvm.tags)
+
+    def test_114_default_audiovm(self):
+        class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
+            default_audiovm = qubes.property('default_audiovm',
+                                             default=(lambda self: 'dom0'))
+
+        holder = MyTestHolder(None)
+        audiovm = self.app.add_new_vm('AppVM', name='sys-audio', audiovm='dom0',
+                                      template=self.template, label='red')
+        appvm = self.app.add_new_vm('AppVM', name='test-vm',
+                                    template=self.template, label='red')
+        holder.default_audiovm = 'sys-audio'
+        self.assertEqual(holder.default_audiovm, 'sys-audio')
+        self.assertIsNotNone(self.app.default_audiovm)
+        self.assertTrue(appvm.property_is_default('audiovm'))
+        self.app.default_audiovm = audiovm
+        self.assertEventFired(holder, 'property-set:default_audiovm',
+                              kwargs={'name': 'default_audiovm',
+                                      'newvalue': 'sys-audio'})
+
+        self.assertIn('audiovm-sys-audio', appvm.tags)
+
+    def test_115_audiovm(self):
+        class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
+            audiovm = qubes.property('audiovm',
+                                     default=(lambda self: 'dom0'))
+
+        holder = MyTestHolder(None)
+        audiovm = self.app.add_new_vm('AppVM', name='sys-audio', audiovm='dom0',
+                                      template=self.template, label='red')
+        guivm = self.app.add_new_vm('AppVM', name='sys-gui', audiovm='dom0',
+                                      template=self.template, label='red')
+        appvm = self.app.add_new_vm('AppVM', name='test-vm', audiovm='dom0',
+                                    template=self.template, label='red')
+        holder.audiovm = 'sys-audio'
+        self.assertEqual(holder.audiovm, 'sys-audio')
+
+        self.assertEventFired(holder, 'property-set:audiovm',
+                              kwargs={'name': 'audiovm',
+                                      'newvalue': 'sys-audio'})
+
+        # Set AudioVM
+        self.assertFalse(appvm.property_is_default('audiovm'))
+        appvm.audiovm = audiovm
+        self.assertIn('audiovm-sys-audio', appvm.tags)
+
+        # Change AudioVM
+        appvm.audiovm = guivm
+        self.assertIn('audiovm-sys-gui', appvm.tags)
+        self.assertNotIn('audiovm-sys-audio', appvm.tags)
+
+        # Empty AudioVM
+        del appvm.audiovm
+        self.assertNotIn('audiovm-sys-gui', appvm.tags)
+        self.assertNotIn('audiovm-sys-audio', appvm.tags)
+        self.assertNotIn('audiovm-', appvm.tags)
+
     def test_200_remove_template(self):
         appvm = self.app.add_new_vm('AppVM', name='test-vm',
                                     template=self.template,

+ 47 - 1
qubes/tests/vm/qubesvm.py

@@ -1819,7 +1819,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
     @unittest.mock.patch('qubes.utils.get_timezone')
     @unittest.mock.patch('qubes.utils.urandom')
     @unittest.mock.patch('qubes.vm.qubesvm.QubesVM.untrusted_qdb')
-    def test_622_qdb_keyboard_layout(self, mock_qubesdb, mock_urandom,
+    def test_622_qdb_guivm_keyboard_layout(self, mock_qubesdb, mock_urandom,
             mock_timezone):
         mock_urandom.return_value = b'A' * 64
         mock_timezone.return_value = 'UTC'
@@ -1840,6 +1840,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
             '"complete"\x09};\x0a\x09xkb_symbols   { include ' \
             '"pc+fr+inet(evdev)"\x09};\x0a\x09xkb_geometry  ' \
             '{ include "pc(pc105)"\x09};\x0a};'
+        guivm.is_running = lambda: True
         vm.events_enabled = True
         test_qubesdb = TestQubesDB()
         mock_qubesdb.write.side_effect = test_qubesdb.write
@@ -1871,6 +1872,51 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase):
             '/connected-ips6': '',
         })
 
+    @unittest.mock.patch('qubes.utils.get_timezone')
+    @unittest.mock.patch('qubes.utils.urandom')
+    @unittest.mock.patch('qubes.vm.qubesvm.QubesVM.untrusted_qdb')
+    def test_623_qdb_audiovm(self, mock_qubesdb, mock_urandom,
+            mock_timezone):
+        mock_urandom.return_value = b'A' * 64
+        mock_timezone.return_value = 'UTC'
+        template = self.get_vm(
+            cls=qubes.vm.templatevm.TemplateVM, name='template')
+        template.netvm = None
+        audiovm = self.get_vm(cls=qubes.vm.appvm.AppVM, template=template,
+            name='sys-audio', qid=2, provides_network=False)
+        vm = self.get_vm(cls=qubes.vm.appvm.AppVM, template=template,
+            name='appvm', qid=3)
+        vm.netvm = None
+        vm.audiovm = audiovm
+        audiovm.is_running = lambda: True
+        vm.events_enabled = True
+        test_qubesdb = TestQubesDB()
+        mock_qubesdb.write.side_effect = test_qubesdb.write
+        mock_qubesdb.rm.side_effect = test_qubesdb.rm
+        vm.create_qdb_entries()
+        self.maxDiff = None
+        self.assertEqual(test_qubesdb.data, {
+            '/name': 'test-inst-appvm',
+            '/type': 'AppVM',
+            '/default-user': 'user',
+            '/qubes-vm-type': 'AppVM',
+            '/qubes-audio-domain-xid': '{}'.format(audiovm.xid),
+            '/qubes-debug-mode': '0',
+            '/qubes-base-template': 'test-inst-template',
+            '/qubes-timezone': 'UTC',
+            '/qubes-random-seed': base64.b64encode(b'A' * 64),
+            '/qubes-vm-persistence': 'rw-only',
+            '/qubes-vm-updateable': 'False',
+            '/qubes-block-devices': '',
+            '/qubes-usb-devices': '',
+            '/qubes-iptables': 'reload',
+            '/qubes-iptables-error': '',
+            '/qubes-iptables-header': unittest.mock.ANY,
+            '/qubes-service/qubes-update-check': '0',
+            '/qubes-service/meminfo-writer': '1',
+            '/connected-ips': '',
+            '/connected-ips6': '',
+        })
 
     @asyncio.coroutine
     def coroutine_mock(self, mock, *args, **kwargs):

+ 8 - 1
qubes/vm/qubesvm.py

@@ -519,6 +519,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
                              default=(lambda self: self.app.default_guivm),
                              doc='VM used for Gui')
 
+    audiovm = qubes.VMProperty('audiovm', load_stage=4, allow_none=True,
+                             default=(lambda self: self.app.default_audiovm),
+                             doc='VM used for Audio')
+
     virt_mode = qubes.property(
         'virt_mode',
         type=str, setter=_setter_virt_mode,
@@ -686,7 +690,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
         if self.libvirt_domain is None:
             return -1
         try:
-            return self.libvirt_domain.ID()
+            if self.is_running():
+                return self.libvirt_domain.ID()
+
+            return -1
         except libvirt.libvirtError as e:
             if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
                 return -1

+ 1 - 0
rpm_spec/core-dom0.spec.in

@@ -276,6 +276,7 @@ fi
 %{python3_sitelib}/qubes/ext/block.py
 %{python3_sitelib}/qubes/ext/core_features.py
 %{python3_sitelib}/qubes/ext/gui.py
+%{python3_sitelib}/qubes/ext/audio.py
 %{python3_sitelib}/qubes/ext/pci.py
 %{python3_sitelib}/qubes/ext/qubesmanager.py
 %{python3_sitelib}/qubes/ext/r3compatibility.py

+ 1 - 0
setup.py

@@ -64,6 +64,7 @@ if __name__ == '__main__':
                 'qubes.ext.core_features = qubes.ext.core_features:CoreFeatures',
                 'qubes.ext.qubesmanager = qubes.ext.qubesmanager:QubesManager',
                 'qubes.ext.gui = qubes.ext.gui:GUI',
+                'qubes.ext.audio = qubes.ext.audio:AUDIO',
                 'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility',
                 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension',
                 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension',