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
This commit is contained in:
commit
16bdeea2c0
@ -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
qubes/ext/audio.py
Normal file
93
qubes/ext/audio.py
Normal file
@ -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))
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
setup.py
1
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',
|
||||
|
Loading…
Reference in New Issue
Block a user