Added gui for booting VM from CDROM/block device.

Available via command line (qubes-vm-boot-from-device) and
from a button in VM Settings/Advanced.
This commit is contained in:
Marta Marczykowska-Górecka 2017-09-08 22:43:43 +02:00
parent 0e5feeac0d
commit 2ac9b1d182
No known key found for this signature in database
GPG Key ID: 9A752C30B26FD04B
7 changed files with 380 additions and 23 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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.')

View File

@ -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

View File

@ -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'
],
})

117
ui/bootfromdevice.ui Normal file
View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BootDialog</class>
<widget class="QDialog" name="BootDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>170</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Boot from device</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Boot qube from device</string>
</property>
<widget class="QWidget" name="">
<property name="geometry">
<rect>
<x>0</x>
<y>30</y>
<width>581</width>
<height>72</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QRadioButton" name="blockDeviceRadioButton">
<property name="text">
<string>from existing block device</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="fileRadioButton">
<property name="text">
<string>from file in qube</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="fileVM">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="1" column="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="pathText"/>
</item>
<item>
<widget class="QPushButton" name="selectFileButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="blockDeviceComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -29,7 +29,7 @@
<locale language="English" country="UnitedStates"/>
</property>
<property name="currentIndex">
<number>0</number>
<number>1</number>
</property>
<widget class="QWidget" name="basic_tab">
<property name="locale">
@ -383,6 +383,99 @@
<string>Advanced</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_9">
<item row="0" column="1" rowspan="2" colspan="2">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="kernel_groupbox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Kernel</string>
</property>
<layout class="QFormLayout" name="formLayout_9">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Kernel:</string>
</property>
<property name="buddy">
<cstring>kernel</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="kernel"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Kernel opts:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="kernel_opts">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>[]</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="other_groupbox">
<property name="title">
<string>Other</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="1" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Default DispVM:</string>
</property>
<property name="buddy">
<cstring>default_dispvm</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="default_dispvm"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="bootFromDeviceButton">
<property name="text">
<string>Boot qube from CDROM</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="enabled">
@ -942,11 +1035,6 @@
<tabstop>vcpus</tabstop>
<tabstop>include_in_balancing</tabstop>
<tabstop>kernel</tabstop>
<tabstop>drive_groupbox</tabstop>
<tabstop>drive_type</tabstop>
<tabstop>drive_domain</tabstop>
<tabstop>drive_path</tabstop>
<tabstop>drive_path_button</tabstop>
<tabstop>policyAllowRadioButton</tabstop>
<tabstop>policyDenyRadioButton</tabstop>
<tabstop>icmpCheckBox</tabstop>