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