Sfoglia il codice sorgente

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.
Marta Marczykowska-Górecka 6 anni fa
parent
commit
2ac9b1d182
7 ha cambiato i file con 380 aggiunte e 23 eliminazioni
  1. 130 0
      qubesmanager/bootfromdevice.py
  2. 6 16
      qubesmanager/settings.py
  3. 29 1
      qubesmanager/utils.py
  4. 3 0
      rpm_spec/qmgr.spec
  5. 1 0
      setup.py
  6. 117 0
      ui/bootfromdevice.ui
  7. 94 6
      ui/settingsdlg.ui

+ 130 - 0
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()

+ 6 - 16
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()

+ 29 - 1
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.')

+ 3 - 0
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

+ 1 - 0
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'
             ],
         })

+ 117 - 0
ui/bootfromdevice.ui

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

+ 94 - 6
ui/settingsdlg.ui

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