diff --git a/qubesmanager/bootfromdevice.py b/qubesmanager/bootfromdevice.py new file mode 100644 index 0000000..477e38d --- /dev/null +++ b/qubesmanager/bootfromdevice.py @@ -0,0 +1,130 @@ +#!/usr/bin/python3 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import subprocess +from . import utils +from .firewall import * +from .ui_bootfromdevice import * +import qubesadmin.tools.qvm_start as qvm_start + + +class VMBootFromDeviceWindow(Ui_BootDialog, QDialog): + def __init__(self, vm, qapp, parent=None): + super(VMBootFromDeviceWindow, self).__init__(parent) + + self.vm = vm + self.qapp = qapp + + self.setupUi(self) + self.setWindowTitle(self.tr("Boot {vm} from device").format(vm=self.vm.name)) + + self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) + self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) + + # populate buttons and such + self.__init_buttons__() + + + def reject(self): + self.done(0) + + def save_and_apply(self): + if self.blockDeviceRadioButton.isChecked(): + cdrom_location = self.blockDeviceComboBox.currentText() + elif self.fileRadioButton.isChecked(): + cdrom_location = self.vm_list[self.fileVM.currentIndex()] + ":" + self.pathText.text() + else: + QMessageBox.warning(None, + self.tr( + "ERROR!"), + self.tr("No file or block device selected; please select one.")) + return + + qvm_start.main(['--cdrom', cdrom_location, self.vm.name]) + + def __init_buttons__(self): + self.fileVM.setEnabled(False) + self.selectFileButton.setEnabled(False) + self.blockDeviceComboBox.setEnabled(False) + + self.blockDeviceRadioButton.clicked.connect(self.radio_button_clicked) + self.fileRadioButton.clicked.connect(self.radio_button_clicked) + self.selectFileButton.clicked.connect(self.select_file_dialog) + + self.vm_list, self.vm_idx = utils.prepare_vm_choice( + self.fileVM, + self.vm, None, + None, + None, + allow_default=False, allow_none=False) + + self.block_list, self.block_idx = utils.prepare_choice( + self.blockDeviceComboBox, + self.vm, + None, + [device for domain in self.vm.app.domains + for device in domain.devices["block"]], + None, + None, + allow_default=False, allow_none=False + ) + + def radio_button_clicked(self): + self.blockDeviceComboBox.setEnabled(self.blockDeviceRadioButton.isChecked()) + self.fileVM.setEnabled(self.fileRadioButton.isChecked()) + self.selectFileButton.setEnabled(self.fileRadioButton.isChecked()) + self.pathText.setEnabled(self.fileRadioButton.isChecked()) + + def select_file_dialog(self): + backend_vm = self.vm_list[self.fileVM.currentIndex()] + + try: + new_path = utils.get_path_from_vm(backend_vm, "qubes.SelectFile") + except subprocess.CalledProcessError: + new_path = None + + if new_path: + self.pathText.setText(new_path) + + +parser = qubesadmin.tools.QubesArgumentParser(vmname_nargs=1) + +def main(args=None): + global bootfromdevice_window + + args = parser.parse_args(args) + vm = args.domains.pop() + + qapp = QApplication(sys.argv) + qapp.setOrganizationName('Invisible Things Lab') + qapp.setOrganizationDomain("https://www.qubes-os.org/") + qapp.setApplicationName("Qubes VM Settings") + +# if not utils.is_debug(): #FIXME +# sys.excepthook = handle_exception + + bootfromdevice_window = VMBootFromDeviceWindow(vm, qapp) + bootfromdevice_window.show() + + qapp.exec_() + qapp.exit() + +if __name__ == "__main__": + main() diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index 5855f51..e29fa54 100755 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -46,6 +46,7 @@ from .backup_utils import get_path_for_vm from .firewall import * from .ui_settingsdlg import * +from .bootfromdevice import main as bootfromdevice class VMSettingsWindow(Ui_SettingsDialog, QDialog): tabs_indices = collections.OrderedDict(( @@ -91,6 +92,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.connect(self.init_mem, SIGNAL("editingFinished()"), self.check_mem_changes) self.connect(self.max_mem_size, SIGNAL("editingFinished()"), self.check_mem_changes) self.drive_path_button.clicked.connect(self.drive_path_button_pressed) + self.bootFromDeviceButton.clicked.connect(self.boot_from_cdrom_button_pressed) ###### firewall tab if self.tabWidget.isTabEnabled(self.tabs_indices['firewall']): @@ -454,22 +456,6 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.vm.features.get('services.meminfo-writer', True)) self.max_mem_size.setEnabled(self.include_in_balancing.isChecked()) - try: - self.root_img_path.setText('{volume.pool}:{volume.vid}'.format( - volume=self.vm.volumes['root'])) - except AttributeError: - self.root_img_path.setText("n/a") - try: - self.volatile_img_path.setText('{volume.pool}:{volume.vid}'.format( - volume=self.vm.volumes['volatile'])) - except AttributeError: - self.volatile_img_path.setText('n/a') - self.private_img_path.setText('{volume.pool}:{volume.vid}'.format( - volume=self.vm.volumes['private'])) - - - #kernel - #in case VM is HVM if hasattr(self.vm, "kernel"): self.kernel_groupbox.setVisible(True) @@ -582,6 +568,10 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): return msg + def boot_from_cdrom_button_pressed(self): + self.save_and_apply() + subprocess.check_call(['qubes-vm-boot-from-device', self.vm.name]) + def drive_path_button_pressed(self): if str(self.drive_domain.currentText()) in ["dom0", "dom0 (current)"]: file_dialog = QFileDialog() diff --git a/qubesmanager/utils.py b/qubesmanager/utils.py index 724611f..abb0dcd 100644 --- a/qubesmanager/utils.py +++ b/qubesmanager/utils.py @@ -22,7 +22,7 @@ import functools import os - +import re import qubesadmin from PyQt4.QtGui import QIcon @@ -135,3 +135,31 @@ def debug(*args, **kwargs): if not is_debug(): return print(*args, **kwargs) + + +def get_path_from_vm(vm, service_name): + """ + Displays a file/directory selection window for the given VM. + + :param vm: vm from which to select path + :param service_name: qubes.SelectFile or qubes.SelectDirectory + :return: path to file, checked for validity + """ + + path_re = re.compile(r"[a-zA-Z0-9/:.,_+=() -]*") + path_max_len = 512 + + if not vm: + return None + stdout, stderr = vm.run_service_for_stdio(service_name) + + untrusted_path = stdout.decode(encoding='ascii')[:path_max_len] + + if len(untrusted_path) == 0: + return None + if path_re.match(untrusted_path): + assert '../' not in untrusted_path + assert '\0' not in untrusted_path + return untrusted_path.strip() + else: + raise ValueError('Unexpected characters in path.') diff --git a/rpm_spec/qmgr.spec b/rpm_spec/qmgr.spec index 6c4302f..c90bdba 100644 --- a/rpm_spec/qmgr.spec +++ b/rpm_spec/qmgr.spec @@ -61,6 +61,7 @@ rm -rf $RPM_BUILD_ROOT /usr/bin/qubes-global-settings /usr/bin/qubes-vm-settings /usr/bin/qubes-vm-create +/usr/bin/qubes-vm-boot-from-device /usr/libexec/qubes-manager/mount_for_backup.sh /usr/libexec/qubes-manager/qvm_about.sh @@ -85,10 +86,12 @@ rm -rf $RPM_BUILD_ROOT %{python3_sitelib}/qubesmanager/create_new_vm.py %{python3_sitelib}/qubesmanager/thread_monitor.py %{python3_sitelib}/qubesmanager/utils.py +%{python3_sitelib}/qubesmanager/bootfromdevice.py %{python3_sitelib}/qubesmanager/resources_rc.py %{python3_sitelib}/qubesmanager/ui_backupdlg.py +%{python3_sitelib}/qubesmanager/ui_bootfromdevice.py %{python3_sitelib}/qubesmanager/ui_globalsettingsdlg.py %{python3_sitelib}/qubesmanager/ui_multiselectwidget.py %{python3_sitelib}/qubesmanager/ui_newappvmdlg.py diff --git a/setup.py b/setup.py index f13126b..cba28dd 100644 --- a/setup.py +++ b/setup.py @@ -21,5 +21,6 @@ if __name__ == '__main__': 'qubes-global-settings = qubesmanager.global_settings:main', 'qubes-vm-settings = qubesmanager.settings:main', 'qubes-vm-create = qubesmanager.create_new_vm:main', + 'qubes-vm-boot-from-device = qubesmanager.bootfromdevice:main' ], }) diff --git a/ui/bootfromdevice.ui b/ui/bootfromdevice.ui new file mode 100644 index 0000000..741ad91 --- /dev/null +++ b/ui/bootfromdevice.ui @@ -0,0 +1,117 @@ + + + BootDialog + + + + 0 + 0 + 600 + 170 + + + + + 400 + 0 + + + + Boot from device + + + + + + + + Boot qube from device + + + + + 0 + 30 + 581 + 72 + + + + + + + from existing block device + + + + + + + from file in qube + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + + + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + + 0 + 0 + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + diff --git a/ui/settingsdlg.ui b/ui/settingsdlg.ui index edd9689..ee24036 100644 --- a/ui/settingsdlg.ui +++ b/ui/settingsdlg.ui @@ -29,7 +29,7 @@ - 0 + 1 @@ -383,6 +383,99 @@ Advanced + + + + QLayout::SetDefaultConstraint + + + 0 + + + + + true + + + + 0 + 0 + + + + Kernel + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Kernel: + + + kernel + + + + + + + + + + Kernel opts: + + + + + + + + 50 + false + + + + [] + + + + + + + + + + + + Other + + + + + + Default DispVM: + + + default_dispvm + + + + + + + + + + Boot qube from CDROM + + + + + + @@ -538,63 +631,6 @@ - - - - true - - - Paths - - - - QFormLayout::AllNonFixedFieldsGrow - - - - - root img: - - - - - - - root_img_path - - - - - - - root volatile img: - - - - - - - volatile_path - - - - - - - private img: - - - - - - - private_path - - - - - - @@ -608,172 +644,6 @@ - - - - Other - - - - - - Default DispVM: - - - default_dispvm - - - - - - - - - - - - - QLayout::SetDefaultConstraint - - - 0 - - - - - true - - - - 0 - 0 - - - - Kernel - - - - QFormLayout::AllNonFixedFieldsGrow - - - - - Kernel: - - - kernel - - - - - - - - - - Kernel opts: - - - - - - - - 50 - false - - - - [] - - - - - - - - - - - 0 - 0 - - - - Additional drive - - - true - - - true - - - - - - - - - - - - Path: - - - - - - - - - - Backend domain: - - - - - - - Type: - - - - - - - ... - - - - - - - true - - - - 75 - true - - - - color: rgb(255, 0, 0); - - - New drive will be used only at next VM startup - - - Qt::AutoText - - - - - - - - @@ -1165,11 +1035,6 @@ vcpus include_in_balancing kernel - drive_groupbox - drive_type - drive_domain - drive_path - drive_path_button policyAllowRadioButton policyDenyRadioButton icmpCheckBox