diff --git a/Makefile b/Makefile index 3457e08..28f3a5a 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,16 @@ rpms: rpm --addsign $(RPMS_DIR)/x86_64/qubes-manager*$(VERSION)*.rpm res: - pyrcc4 -o qubesmanager/qrc_resources.py resources.qrc + pyrcc4 -o qubesmanager/resources_rc.py resources.qrc + pyuic4 -o qubesmanager/ui_mainwindow.py mainwindow.ui pyuic4 -o qubesmanager/ui_newappvmdlg.py newappvmdlg.ui pyuic4 -o qubesmanager/ui_editfwrulesdlg.py editfwrulesdlg.ui pyuic4 -o qubesmanager/ui_newfwruledlg.py newfwruledlg.ui + pyuic4 -o qubesmanager/ui_multiselectwidget.py multiselectwidget.ui + pyuic4 -o qubesmanager/ui_settingsdlg.py settingsdlg.ui + pyuic4 -o qubesmanager/ui_restoredlg.py restoredlg.ui + pyuic4 -o qubesmanager/ui_backupdlg.py backupdlg.ui + pyuic4 -o qubesmanager/ui_globalsettingsdlg.py globalsettingsdlg.ui update-repo-current: ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current/dom0/rpm/ diff --git a/backupdlg.ui b/backupdlg.ui new file mode 100644 index 0000000..522ce78 --- /dev/null +++ b/backupdlg.ui @@ -0,0 +1,196 @@ + + + Backup + + + + 0 + 0 + 700 + 399 + + + + Qubes Backup VMs + + + + + + QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage + + + + + + + + 9 + 50 + false + false + false + + + + Select VMs to backup: + + + + + + + + + + + Backup destination directory + + + + + + Device: + + + + + + + + 0 + 0 + + + + + dev1 + + + + + longdeviceblablabla + + + + + dev2 + + + + + dev3 + + + + + + + + Backup directory: + + + + + + + + + + ... + + + + + + + + + + + + + + + 9 + 50 + false + false + false + + + + You're about to perform the following actions: + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> + + + + + + + + 9 + 50 + false + false + false + + + + To continue press Next. + + + + + + + + + + + + 9 + 50 + false + false + false + + + + Backup in progress... + + + + + + + 24 + + + Qt::AlignCenter + + + false + + + + + + + + + diff --git a/globalsettingsdlg.ui b/globalsettingsdlg.ui new file mode 100644 index 0000000..8718e50 --- /dev/null +++ b/globalsettingsdlg.ui @@ -0,0 +1,224 @@ + + + GlobalSettings + + + + 0 + 0 + 678 + 288 + + + + Qubes Global Settings + + + + + + false + + + System defaults + + + + + + + 0 + 0 + + + + UpdateVM: + + + + + + + + + + ClockVM: + + + + + + + + + + Default netVM: + + + + + + + + + + + 0 + 0 + + + + Default template: + + + + + + + + + + + + + false + + + Default memory settings + + + + + 11 + 26 + 134 + 16 + + + + Minimal VM's memory: + + + + + + 11 + 57 + 139 + 16 + + + + dom0 memory margin: + + + + + + 156 + 26 + 121 + 25 + + + + MB + + + 999999999 + + + 10 + + + + + + 156 + 57 + 121 + 25 + + + + MB + + + 999999999 + + + 50 + + + + + + + + false + + + Kernel + + + + + + Default kernel: + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + GlobalSettings + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + GlobalSettings + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/icons/add.png b/icons/add.png new file mode 100644 index 0000000..a15dd10 Binary files /dev/null and b/icons/add.png differ diff --git a/icons/appsprefs.png b/icons/appsprefs.png new file mode 100644 index 0000000..71324c7 Binary files /dev/null and b/icons/appsprefs.png differ diff --git a/icons/edit.png b/icons/edit.png new file mode 100644 index 0000000..b17a7ce Binary files /dev/null and b/icons/edit.png differ diff --git a/icons/flag-blue.png b/icons/flag-blue.png new file mode 100644 index 0000000..6f6b4ab Binary files /dev/null and b/icons/flag-blue.png differ diff --git a/icons/flag-green.png b/icons/flag-green.png new file mode 100644 index 0000000..9b0b06d Binary files /dev/null and b/icons/flag-green.png differ diff --git a/icons/flag-red.png b/icons/flag-red.png new file mode 100644 index 0000000..3a9b9ac Binary files /dev/null and b/icons/flag-red.png differ diff --git a/icons/flag-yellow.png b/icons/flag-yellow.png new file mode 100644 index 0000000..ab7835a Binary files /dev/null and b/icons/flag-yellow.png differ diff --git a/icons/mount.png b/icons/mount.png new file mode 100644 index 0000000..8c84158 Binary files /dev/null and b/icons/mount.png differ diff --git a/icons/newfirewall.png b/icons/newfirewall.png new file mode 100644 index 0000000..85c3602 Binary files /dev/null and b/icons/newfirewall.png differ diff --git a/icons/on.png b/icons/on.png new file mode 100644 index 0000000..8bb39f8 Binary files /dev/null and b/icons/on.png differ diff --git a/icons/pencil.png b/icons/pencil.png new file mode 100644 index 0000000..1ecb10c Binary files /dev/null and b/icons/pencil.png differ diff --git a/icons/redfirewall.png b/icons/redfirewall.png new file mode 100644 index 0000000..337dec8 Binary files /dev/null and b/icons/redfirewall.png differ diff --git a/icons/redfirewall2.png b/icons/redfirewall2.png new file mode 100644 index 0000000..e611d89 Binary files /dev/null and b/icons/redfirewall2.png differ diff --git a/icons/remove.png b/icons/remove.png new file mode 100644 index 0000000..fffb248 Binary files /dev/null and b/icons/remove.png differ diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..0de6d08 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,534 @@ + + + VmManagerWindow + + + + 0 + 0 + 821 + 600 + + + + + 0 + 0 + + + + Qt::DefaultContextMenu + + + Qubes VM Manager + + + + :/qubes.png:/qubes.png + + + + + + + true + + + + 0 + 0 + + + + false + + + true + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 200 + 30 + + + + Qt::CustomContextMenu + + + false + + + 0 + + + Qt::ScrollBarAlwaysOn + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + Qt::NoPen + + + true + + + false + + + 4 + + + false + + + 150 + + + 150 + + + false + + + false + + + + Nowy wiersz + + + + + + + + Name + + + VM name + + + + + Upd + + + Update info + + + + + Template + + + VM's template + + + + + NetVM + + + VM's netVM + + + + + CPU + + + + + CPU Graph + + + CPU usage graph + + + + + MEM + + + + + MEM Graph + + + Memory usage graph + + + + + + + + + + 0 + 0 + 821 + 23 + + + + + Options + + + + + + + + View + + + + + + + + + + + + + + + toolBar + + + Qt::BottomToolBarArea|Qt::TopToolBarArea + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + + :/createvm.png:/createvm.png + + + Create AppVM + + + Create a new AppVM + + + + + false + + + + :/removevm.png:/removevm.png + + + remove AppVM + + + Remove an existing AppVM (must be stopped first) + + + + + false + + + + :/resumevm.png:/resumevm.png + + + Start/Resume VM + + + Start/Resume a VM + + + + + false + + + + :/pausevm.png:/pausevm.png + + + Pause VM + + + Pause a running VM + + + + + false + + + + :/shutdownvm.png:/shutdownvm.png + + + Shutdown VM + + + Shutdown a running VM + + + + + false + + + + :/appsprefs.png:/appsprefs.png + + + Select VM applications + + + Select applications present in menu for this VM + + + + + false + + + + :/updateable.png:/updateable.png + + + Update VM + + + Update VM system + + + + + true + + + true + + + + :/showallvms.png + :/showallvms.png:/showallvms.png + + + Show/Hide inactive VMs + + + Show/Hide inactive VMs + + + + + + :/redfirewall.png:/redfirewall.png + + + Edit VM Firewall rules + + + Edit VM Firewall rules + + + + + true + + + + :/showcpuload.png:/showcpuload.png + + + Show graphs + + + Show Graphs + + + + + Options + + + + + View + + + + + true + + + true + + + CPU + + + + + true + + + true + + + CPU Graph + + + + + true + + + true + + + MEM + + + + + true + + + true + + + MEM Graph + + + + + true + + + true + + + Template + + + + + true + + + true + + + NetVM + + + + + + :/root.png:/root.png + + + Settings + + + VM Settings + + + + + Restore VMs from backup + + + + + Backup VMs + + + + + Global settings + + + + + true + + + true + + + Upd + + + + + + + + diff --git a/multiselectwidget.ui b/multiselectwidget.ui new file mode 100644 index 0000000..f5e0ab6 --- /dev/null +++ b/multiselectwidget.ui @@ -0,0 +1,193 @@ + + + MultiSelectWidget + + + + 0 + 0 + 602 + 459 + + + + Form + + + + + + Qt::Horizontal + + + + 10 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Available + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + >> + + + + + + + > + + + + + + + < + + + + + + + << + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Selected + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Qt::Horizontal + + + + 10 + 20 + + + + + + + + + diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py index f0800b3..40ece49 100755 --- a/qubesmanager/appmenu_select.py +++ b/qubesmanager/appmenu_select.py @@ -33,235 +33,87 @@ from qubes.qubes import QubesDaemonPidfile from qubes.qubes import QubesHost from qubes.qubes import qrexec_client_path -import qubesmanager.qrc_resources +import qubesmanager.resources_rc from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent import subprocess import time -import threading + from operator import itemgetter +from thread_monitor import * +from multiselectwidget import * + whitelisted_filename = 'whitelisted-appmenus.list' -class AppRowInTable(object): - def __init__(self, filename, name, row_no, table): +class AppListWidgetItem(QListWidgetItem): + def __init__(self, name, filename, parent = None): + super(AppListWidgetItem, self).__init__(name, parent) self.filename = filename - self.row_no = row_no - - table.setRowHeight (row_no, AppmenuSelectWindow.row_height) - - self.name_widget = QTableWidgetItem(name) - self.name_widget.setFlags (Qt.ItemIsSelectable | Qt.ItemIsEnabled ) - table.setItem(row_no, 0, self.name_widget) - - self.appvm_widget = QCheckBox() - table.setCellWidget(row_no, 1, self.appvm_widget) - -class ThreadMonitor(QObject): - def __init__(self): - self.success = True - self.error_msg = None - self.event_finished = threading.Event() - - def set_error_msg(self, error_msg): - self.success = False - self.error_msg = error_msg - self.set_finished() - - def is_finished(self): - return self.event_finished.is_set() - - def set_finished(self): - self.event_finished.set() -class AppmenuSelectWindow(QDialog): - row_height = 20 - def __init__(self, vm, parent=None): - super(AppmenuSelectWindow, self).__init__(parent) - - self.gridLayout = QGridLayout(self) - - self.buttonBox = QDialogButtonBox(self) - self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) - self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) - self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) - - self.table = QTableWidget(self) - self.table.clear() - self.table.setColumnCount(2) - self.table.setColumnWidth (0, 200) - self.table.setColumnWidth (1, 40) - - self.table.horizontalHeader().setResizeMode(QHeaderView.Stretch) - self.table.horizontalHeader().setResizeMode(1, QHeaderView.Fixed) - self.table.setAlternatingRowColors(True) - self.table.verticalHeader().hide() - self.table.horizontalHeader().show() - self.table.setGridStyle(Qt.NoPen) - self.table.setSortingEnabled(True) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setSelectionMode(QTableWidget.SingleSelection) - - self.gridLayout.addWidget(self.table, 0, 0, 1, 1) - self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) +class AppmenuSelectManager: + def __init__(self, vm, apps_multiselect, parent=None): + self.app_list = apps_multiselect # this is a multiselect wiget + self.vm = vm if self.vm.template_vm: self.source_vm = self.vm.template_vm else: self.source_vm = self.vm - self.setWindowTitle("Qubes Appmenus for %s" % vm.name) - self.resize(250,500) - self.fill_table() - self.load_list_of_selected() + self.fill_apps_list() - def reject(self): - self.done(0) - - def addActions(self, target, actions): - for action in actions: - if action is None: - target.addSeparator() - else: - target.addAction(action) - - def createAction(self, text, slot=None, shortcut=None, icon=None, - tip=None, checkable=False, signal="triggered()"): - action = QAction(text, self) - if icon is not None: - action.setIcon(QIcon(":/%s.png" % icon)) - if shortcut is not None: - action.setShortcut(shortcut) - if tip is not None: - action.setToolTip(tip) - action.setStatusTip(tip) - if slot is not None: - self.connect(action, SIGNAL(signal), slot) - if checkable: - action.setCheckable(True) - return action - - def fill_table(self): + def fill_apps_list(self): template_dir = self.source_vm.appmenus_templates_dir template_file_list = os.listdir(template_dir) - self.table.clear() - self.table.setHorizontalHeaderLabels(['Name', 'VM']) - self.table.setRowCount(len(template_file_list)) + whitelisted = [] + if os.path.exists(self.vm.dir_path + '/' + whitelisted_filename): + f = open(self.vm.dir_path + '/' + whitelisted_filename, 'r') + whitelisted = [item.strip() for item in f] + f.close() - row_no = 0 - appmenus = [] + self.app_list.clear() + + + available_appmenus = [] for template_file in template_file_list: desktop_template = open(template_dir + '/' + template_file, 'r') for line in desktop_template: if line.startswith("Name=%VMNAME%: "): desktop_name = line.partition('Name=%VMNAME%: ')[2].strip() - row = AppRowInTable (template_file, desktop_name, row_no, self.table) - appmenus.append(row) - row_no += 1 + available_appmenus.append( (template_file, desktop_name) ) break desktop_template.close() - self.table.setRowCount(row_no) - self.appmenus = appmenus - self.table.sortItems(0) + whitelisted_appmenus = [a for a in available_appmenus if a[0] in whitelisted] + available_appmenus = [a for a in available_appmenus if a[0] not in whitelisted] + + for a in available_appmenus: + self.app_list.available_list.addItem( AppListWidgetItem(a[1], a[0])) - def load_list_of_selected(self): - if not os.path.exists(self.vm.dir_path + '/' + whitelisted_filename): - # select none - for row in self.appmenus: - row.appvm_widget.setCheckState(Qt.Unchecked) - return - - f = open(self.vm.dir_path + '/' + whitelisted_filename, 'r') - whitelisted = [item.strip() for item in f] - f.close() - for row in self.appmenus: - if row.filename in whitelisted: - row.appvm_widget.setCheckState(Qt.Checked) - else: - row.appvm_widget.setCheckState(Qt.Unchecked) + for a in whitelisted_appmenus: + self.app_list.selected_list.addItem( AppListWidgetItem(a[1], a[0])) + + self.app_list.available_list.sortItems() + self.app_list.selected_list.sortItems() def save_list_of_selected(self): whitelisted = open(self.vm.dir_path + '/' + whitelisted_filename, 'w') - for row in self.appmenus: - if row.appvm_widget.checkState() == Qt.Checked: - whitelisted.write(row.filename + '\n') - whitelisted.close() + for i in range(self.app_list.selected_list.count()): + item = self.app_list.selected_list.item(i) + whitelisted.write(item.filename + '\n') + whitelisted.close() + - def save_and_apply(self): + def save_appmenu_select_changes(self): self.save_list_of_selected() subprocess.check_call([qubes_appmenu_remove_cmd, self.vm.name]) subprocess.check_call([qubes_appmenu_create_cmd, self.source_vm.appmenus_templates_dir, self.vm.name]) - self.done(0) - -# Bases on the original code by: -# Copyright (c) 2002-2007 Pascal Varet - -def handle_exception( exc_type, exc_value, exc_traceback ): - import sys - import os.path - import traceback - - filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() - filename = os.path.basename( filename ) - error = "%s: %s" % ( exc_type.__name__, exc_value ) - - QMessageBox.critical(None, "Houston, we have a problem...", - "Whoops. A critical error has occured. This is most likely a bug " - "in Qubes Appmenu Select application.

" - "%s" % error + - "at line %d of file %s.

" - % ( line, filename )) - - #sys.exit(1) - -def main(): - - - global qubes_host - qubes_host = QubesHost() - - global app - app = QApplication(sys.argv) - app.setOrganizationName("The Qubes Project") - app.setOrganizationDomain("http://qubes-os.org") - app.setApplicationName("Qubes Appmenu Select") - app.setWindowIcon(QIcon(":/qubes.png")) - - sys.excepthook = handle_exception - - qvm_collection = QubesVmCollection() - qvm_collection.lock_db_for_reading() - qvm_collection.load() - qvm_collection.unlock_db() - - vm = None - - if len(sys.argv) > 1: - vm = qvm_collection.get_vm_by_name(sys.argv[1]) - if vm is None or vm.qid not in qvm_collection: - QMessageBox.critical(None, "Qubes Appmenu Select Error", - "A VM with the name '{0}' does not exist in the system.".format(sys.argv[1])) - sys.exit(1) - else: - vms_list = [vm.name for vm in qvm_collection.values() if (vm.is_appvm() or vm.is_template())] - vmname = QInputDialog.getItem(None, "Select VM", "Select VM:", vms_list, editable = False) - if not vmname[1]: - sys.exit(1) - vm = qvm_collection.get_vm_by_name(vmname[0]) - - global manager_window - select_window = AppmenuSelectWindow(vm) - - select_window.show() - - app.exec_() - app.exit() diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py new file mode 100644 index 0000000..ef3623d --- /dev/null +++ b/qubesmanager/backup.py @@ -0,0 +1,239 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# 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 sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost +from qubes import qubesutils + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +from thread_monitor import * +from operator import itemgetter + +from datetime import datetime +from string import replace + +from ui_backupdlg import * +from multiselectwidget import * + +from backup_utils import * + + +class BackupVMsWindow(Ui_Backup, QWizard): + + __pyqtSignals__ = ("backup_progress(int)",) + + excluded = [] + to_backup = [] + + def __init__(self, app, qvm_collection, blk_manager, parent=None): + super(BackupVMsWindow, self).__init__(parent) + + self.app = app + self.qvm_collection = qvm_collection + self.blk_manager = blk_manager + + self.dev_mount_path = None + self.backup_dir = None + self.func_output = [] + + for vm in self.qvm_collection.values(): + if vm.qid == 0: + self.vm = vm + break; + + assert self.vm != None + + self.setupUi(self) + + self.dir_line_edit.setReadOnly(True) + + self.select_vms_widget = MultiSelectWidget(self) + self.verticalLayout.insertWidget(1, self.select_vms_widget) + + self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) + self.connect(self.dev_combobox, SIGNAL("activated(int)"), self.dev_combobox_activated) + self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue) + + self.select_vms_page.isComplete = self.has_selected_vms + self.select_dir_page.isComplete = self.has_selected_dir + #FIXME + #this causes to run isComplete() twice, I don't know why + self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) + + self.__fill_vms_list__() + fill_devs_list(self) + + def __fill_vms_list__(self): + for vm in self.qvm_collection.values(): + if vm.is_running() and vm.qid != 0: + self.excluded.append(vm.name) + continue + + if vm.is_appvm() and vm.internal: + self.excluded.append(vm.name) + continue + + if vm.is_template() and vm.installed_by_rpm: + self.excluded.append(vm.name) + continue + + self.to_backup.append(vm.name) + self.select_vms_widget.available_list.addItem(vm.name) + + + def dev_combobox_activated(self, idx): + dev_combobox_activated(self, idx) + + + @pyqtSlot(name='on_select_path_button_clicked') + def select_path_button_clicked(self): + select_path_button_clicked(self) + + def validateCurrentPage(self): + if self.currentPage() is self.select_vms_page: + for i in range(self.select_vms_widget.available_list.count()): + vmname = self.select_vms_widget.available_list.item(i).text() + self.excluded.append(vmname) + return True + + def gather_output(self, s): + self.func_output.append(s) + + def update_progress_bar(self, value): + self.emit(SIGNAL("backup_progress(int)"), value) + + + def __do_backup__(self, thread_monitor): + msg = [] + try: + qubesutils.backup_do(str(self.backup_dir), self.files_to_backup, self.update_progress_bar) + #simulate_long_lasting_proces(10, self.update_progress_bar) + except Exception as ex: + msg.append(str(ex)) + + if len(msg) > 0 : + thread_monitor.set_error_msg('\n'.join(msg)) + + thread_monitor.set_finished() + + + def current_page_changed(self, id): + if self.currentPage() is self.confirm_page: + del self.func_output[:] + self.files_to_backup = qubesutils.backup_prepare(str(self.backup_dir), exclude_list = self.excluded, print_callback = self.gather_output) + + self.textEdit.setReadOnly(True) + self.textEdit.setFontFamily("Monospace") + self.textEdit.setText("\n".join(self.func_output)) + + elif self.currentPage() is self.commit_page: + self.button(self.CancelButton).setDisabled(True) + self.button(self.FinishButton).setDisabled(True) + self.thread_monitor = ThreadMonitor() + thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,)) + thread.daemon = True + thread.start() + + while not self.thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + if not self.thread_monitor.success: + QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) + + umount_device(self.dev_mount_path) + self.button(self.FinishButton).setEnabled(True) + + def has_selected_vms(self): + return self.select_vms_widget.selected_list.count() > 0 + + def has_selected_dir(self): + return self.backup_dir != None + + + + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes Restore VMs application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes Backup VMs") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + global backup_window + backup_window = BackupVMsWindow() + + backup_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/backup_utils.py b/qubesmanager/backup_utils.py new file mode 100644 index 0000000..0978b03 --- /dev/null +++ b/qubesmanager/backup_utils.py @@ -0,0 +1,159 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# +# 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 sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time + +from thread_monitor import * + +from datetime import datetime +from string import replace + + +def check_if_mounted(dev_path): + mounts_file = open("/proc/mounts") + for m in list(mounts_file): + if m.startswith(dev_path): + return m.split(" ")[1] + return None + + +def mount_device(dev_path): + try: + mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0] + pmount_cmd = ["pmount", dev_path, mount_dir_name] + res = subprocess.check_call(pmount_cmd) + except Exception as ex: + QMessageBox.warning (None, "Error mounting selected device!", "ERROR: {0}".format(ex)) + return None + if res == 0: + dev_mount_path = "/media/"+mount_dir_name + return dev_mount_path + return None + +def umount_device(dev_mount_path): + try: + pumount_cmd = ["pumount", dev_mount_path] + res = subprocess.check_call(pumount_cmd) + if res == 0: + dev_mount_path = None + except Exception as ex: + QMessageBox.warning (None, "Could not unmount backup device!", "ERROR: {0}".format(ex)) + return dev_mount_path + + +def fill_devs_list(dialog): + dialog.dev_combobox.clear() + dialog.dev_combobox.addItem("None") + for a in dialog.blk_manager.attached_devs: + if dialog.blk_manager.attached_devs[a]['attached_to']['vm'] == dialog.vm.name : + att = a + " " + unicode(dialog.blk_manager.attached_devs[a]['size']) + " " + dialog.blk_manager.attached_devs[a]['desc'] + dialog.dev_combobox.addItem(att, QVariant(a)) + for a in dialog.blk_manager.free_devs: + att = a + " " + unicode(dialog.blk_manager.free_devs[a]['size']) + " " + dialog.blk_manager.free_devs[a]['desc'] + dialog.dev_combobox.addItem(att, QVariant(a)) + dialog.dev_combobox.setCurrentIndex(0) #current selected is null "" + dialog.prev_dev_idx = 0 + dialog.dir_line_edit.clear() + dialog.dir_line_edit.setEnabled(False) + dialog.select_path_button.setEnabled(False) + + +def enable_dir_line_edit(dialog, boolean): + dialog.dir_line_edit.setEnabled(boolean) + dialog.select_path_button.setEnabled(boolean) + + +def dev_combobox_activated(dialog, idx): + if idx == dialog.prev_dev_idx: #nothing has changed + return + #there was a change + + dialog.dir_line_edit.setText("") + dialog.backup_dir = None + + if dialog.dev_mount_path != None: + dialog.dev_mount_path = umount_device(dialog.dev_mount_path) + if dialog_dev_mount_path != None: + dialog.dev_combobox.setCurrentIndex(dialog.prev_dev_idx) + return + + enable_dir_line_edit(dialog, False) + + if dialog.dev_combobox.currentText() != "None": #An existing device chosen + dev_name = str(dialog.dev_combobox.itemData(idx).toString()) + + if dev_name in dialog.blk_manager.free_devs: + if dev_name.startswith(dialog.vm.name): # originally attached to dom0 + dev_path = "/dev/"+dev_name.split(":")[1] + + else: # originally attached to another domain, eg. usbvm + #attach it to dom0, then treat it as an attached device + dialog.blk_manager.attach_device(dialog.vm, dev_name) + + if dev_name in dialog.blk_manager.attached_devs: #is attached to dom0 + assert dialog.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == dialog.vm.name + dev_path = "/dev/" + dialog.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] + + #check if device mounted + dialog.dev_mount_path = check_if_mounted(dev_path) + if dialog.dev_mount_path != None: + enable_dir_line_edit(dialog, True) + else: + dialog.dev_mount_path = mount_device(dev_path) + if dialog.dev_mount_path != None: + enable_dir_line_edit(dialog, True) + else: + dialog.dev_combobox.setCurrentIndex(0) #if couldn't mount - set current device to "None" + + + dialog.prev_dev_idx = idx + dialog.select_dir_page.emit(SIGNAL("completeChanged()")) + + +def select_path_button_clicked(dialog): + dialog.backup_dir = dialog.dir_line_edit.text() + file_dialog = QFileDialog() + file_dialog.setReadOnly(True) + new_path = file_dialog.getExistingDirectory(dialog, "Select backup directory.", dialog.dev_mount_path) + if new_path: + dialog.dir_line_edit.setText(new_path) + dialog.backup_dir = new_path + dialog.select_dir_page.emit(SIGNAL("completeChanged()")) + + + +def simulate_long_lasting_proces(period, progress_callback): + for i in range(period): + progress_callback((i*100)/period) + time.sleep(1) + + progress_callback(100) + return 0 + diff --git a/qubesmanager/firewall.py b/qubesmanager/firewall.py index aef3493..30f4f54 100644 --- a/qubesmanager/firewall.py +++ b/qubesmanager/firewall.py @@ -31,106 +31,8 @@ from qubes.qubes import QubesVmCollection from qubes.qubes import QubesException from qubes.qubes import dry_run -import ui_editfwrulesdlg import ui_newfwruledlg -class EditFwRulesDlg (QDialog, ui_editfwrulesdlg.Ui_EditFwRulesDlg): - def __init__(self, parent = None): - super (EditFwRulesDlg, self).__init__(parent) - self.setupUi(self) - self.newRuleButton.clicked.connect(self.new_rule_button_pressed) - self.editRuleButton.clicked.connect(self.edit_rule_button_pressed) - self.deleteRuleButton.clicked.connect(self.delete_rule_button_pressed) - self.policyAllowRadioButton.toggled.connect(self.policy_radio_toggled) - self.dnsCheckBox.toggled.connect(self.dns_checkbox_toggled) - self.icmpCheckBox.toggled.connect(self.icmp_checkbox_toggled) - - def set_model(self, model): - self.__model = model - self.rulesTreeView.setModel(model) - self.rulesTreeView.header().setResizeMode(QHeaderView.ResizeToContents) - self.rulesTreeView.header().setResizeMode(0, QHeaderView.Stretch) - self.set_allow(model.allow) - self.dnsCheckBox.setChecked(model.allowDns) - self.icmpCheckBox.setChecked(model.allowIcmp) - self.setWindowTitle(model.get_vm_name() + " firewall") - - def set_allow(self, allow): - self.policyAllowRadioButton.setChecked(allow) - self.policyDenyRadioButton.setChecked(not allow) - - def policy_radio_toggled(self, on): - self.__model.allow = self.policyAllowRadioButton.isChecked() - - def dns_checkbox_toggled(self, on): - self.__model.allowDns = on - - def icmp_checkbox_toggled(self, on): - self.__model.allowIcmp = on - - def new_rule_button_pressed(self): - dialog = NewFwRuleDlg() - self.run_rule_dialog(dialog) - - def edit_rule_button_pressed(self): - dialog = NewFwRuleDlg() - dialog.set_ok_enabled(True) - selected = self.rulesTreeView.selectedIndexes() - if len(selected) > 0: - row = self.rulesTreeView.selectedIndexes().pop().row() - address = self.__model.get_column_string(0, row).replace(' ', '') - dialog.addressComboBox.setItemText(0, address) - dialog.addressComboBox.setCurrentIndex(0) - service = self.__model.get_column_string(1, row) - dialog.serviceComboBox.setItemText(0, service) - dialog.serviceComboBox.setCurrentIndex(0) - self.run_rule_dialog(dialog, row) - - def run_rule_dialog(self, dialog, row = None): - if dialog.exec_(): - address = str(dialog.addressComboBox.currentText()) - service = str(dialog.serviceComboBox.currentText()) - port = None - port2 = None - - unmask = address.split("/", 1) - if len(unmask) == 2: - address = unmask[0] - netmask = int(unmask[1]) - else: - netmask = 32 - - if address == "*": - address = "0.0.0.0" - netmask = 0 - - if service == "*": - service = "0" - try: - range = service.split("-", 1) - if len(range) == 2: - port = int(range[0]) - port2 = int(range[1]) - else: - port = int(service) - except (TypeError, ValueError) as ex: - port = self.__model.get_service_port(service) - - if port is not None: - if port2 is not None and port2 <= port: - QMessageBox.warning(None, "Invalid service ports range", "Port {0} is lower than port {1}.".format(port2, port)) - else: - item = QubesFirewallRuleItem(address, netmask, port, port2) - if row is not None: - self.__model.setChild(row, item) - else: - self.__model.appendChild(item) - else: - QMessageBox.warning(None, "Invalid service name", "Service '{0} is unknown.".format(service)) - - def delete_rule_button_pressed(self): - for i in set([index.row() for index in self.rulesTreeView.selectedIndexes()]): - self.__model.removeChild(i) class QIPAddressValidator(QValidator): def __init__(self, parent = None): @@ -397,3 +299,4 @@ class QubesFirewallRulesModel(QAbstractItemModel): def __len__(self): return len(self.children) + diff --git a/qubesmanager/global_settings.py b/qubesmanager/global_settings.py new file mode 100644 index 0000000..340210b --- /dev/null +++ b/qubesmanager/global_settings.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# 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 sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +import threading +from operator import itemgetter + +from ui_globalsettingsdlg import * + +class GlobalSettingsWindow(Ui_GlobalSettings, QDialog): + + def __init__(self, parent=None): + super(GlobalSettingsWindow, self).__init__(parent) + + self.setupUi(self) + + def reject(self): + self.done(0) + + def save_and_apply(self): + pass + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes Global Settings application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes Global Settings") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + global global_window + global_window = GlobalSettingsWindow() + + global_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/main.py b/qubesmanager/main.py index c97f7c3..1ed9384 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -35,17 +35,19 @@ from qubes.qubes import QubesDaemonPidfile from qubes.qubes import QubesHost from qubes import qubesutils -import qubesmanager.qrc_resources +import qubesmanager.resources_rc import ui_newappvmdlg -from appmenu_select import AppmenuSelectWindow - -from firewall import EditFwRulesDlg, QubesFirewallRulesModel +from ui_mainwindow import * +from settings import VMSettingsWindow +from restore import RestoreVMsWindow +from backup import BackupVMsWindow +from global_settings import GlobalSettingsWindow +from thread_monitor import * from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent import subprocess import time -import threading from datetime import datetime,timedelta updates_stat_file = 'last_update.stat' @@ -99,76 +101,265 @@ class VmStatusIcon(QLabel): class VmInfoWidget (QWidget): + class VmInfoItem (QTableWidgetItem): + def __init__(self, name, qid): + super(VmInfoWidget.VmInfoItem, self).__init__() + self.value = (name, qid) + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + return self.value[0] < other.value[0] #compare vm.name + + def __init__(self, vm, parent = None): super (VmInfoWidget, self).__init__(parent) - layout0 = QHBoxLayout() + layout = QHBoxLayout () self.label_name = QLabel (vm.name) - - self.vm_running = vm.last_power_state - layout0.addWidget(self.label_name, alignment=Qt.AlignLeft) - - layout1 = QHBoxLayout() - - if vm.template_vm is not None: - self.label_tmpl = QLabel ("" + (vm.template_vm.name) + "") - elif vm.is_appvm(): # and vm.template_vm is None - self.label_tmpl = QLabel ("StandaloneVM") - elif vm.is_template(): - self.label_tmpl = QLabel ("TemplateVM") - elif vm.qid == 0: - self.label_tmpl = QLabel ("AdminVM") - elif vm.is_netvm(): - self.label_tmpl = QLabel ("NetVM") - else: - self.label_tmpl = QLabel ("") - - label_icon_networked = self.set_icon(":/networking.png", vm.is_networked()) - layout1.addWidget(label_icon_networked, alignment=Qt.AlignLeft) - - if vm.is_updateable(): - label_icon_updtbl = self.set_icon(":/updateable.png", True) - layout1.addWidget(label_icon_updtbl, alignment=Qt.AlignLeft) - - layout1.addWidget(self.label_tmpl, alignment=Qt.AlignLeft) - - layout1.addStretch() - - layout2 = QVBoxLayout () - layout2.addLayout(layout0) - layout2.addLayout(layout1) - - layout3 = QHBoxLayout () self.vm_icon = VmStatusIcon(vm) - layout3.addWidget(self.vm_icon) - layout3.addSpacing (10) - layout3.addLayout(layout2) + self.blk_icon = VmIconWidget(":/mount.png") - self.setLayout(layout3) + layout.addWidget(self.vm_icon) + layout.addSpacing (10) + layout.addWidget(self.label_name, alignment=Qt.AlignLeft) + layout.addSpacing (10) + layout.addWidget(self.blk_icon, alignment=Qt.AlignRight) - self.previous_outdated = False - self.previous_update_recommended = False + self.setLayout(layout) + + self.blk_icon.setVisible(False) + + self.tableItem = self.VmInfoItem(vm.name, vm.qid) + + def update_vm_state (self, vm, blk_visible): + self.vm_icon.update() + if blk_visible != None: + self.blk_icon.setVisible(blk_visible) + + + + +class VmTemplateItem (QTableWidgetItem): + def __init__(self, vm): + super(VmTemplateItem, self).__init__() + + if vm.template_vm is not None: + self.setText(vm.template_vm.name) + else: + font = QFont() + font.setStyle(QFont.StyleItalic) + self.setFont(font) + self.setTextColor(QColor("gray")) + + if vm.is_appvm(): # and vm.template_vm is None + self.setText("StandaloneVM") + elif vm.is_template(): + self.setText("TemplateVM") + elif vm.qid == 0: + self.setText("AdminVM") + elif vm.is_netvm(): + self.setText("NetVM") + else: + self.setText("---") + + self.setTextAlignment(Qt.AlignHCenter) + + +class VmIconWidget (QWidget): + def __init__(self, icon_path, enabled=True, parent=None): + super(VmIconWidget, self).__init__(parent) - def set_icon(self, icon_path, enabled = True): label_icon = QLabel() icon = QIcon (icon_path) - icon_sz = QSize (VmManagerWindow.row_height * 0.3, VmManagerWindow.row_height * 0.3) + icon_sz = QSize (VmManagerWindow.row_height * 0.7, VmManagerWindow.row_height * 0.7) icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal) label_icon.setPixmap (icon_pixmap) label_icon.setFixedSize (icon_sz) - return label_icon + + layout = QVBoxLayout() + layout.addWidget(label_icon) + layout.setContentsMargins(0,0,0,0) + self.setLayout(layout) - def update_vm_state (self, vm): - self.vm_icon.update() + +class VmNetvmItem (QTableWidgetItem): + def __init__(self, vm): + super(VmNetvmItem, self).__init__() + + if vm.is_netvm() and not vm.is_proxyvm(): + self.setText("n/a") + elif vm.netvm_vm is not None: + self.setText(vm.netvm_vm.name) + else: + self.setText("---") + + self.setTextAlignment(Qt.AlignHCenter) + + +class VmUsageBarWidget (QWidget): + + class VmUsageBarItem (QTableWidgetItem): + def __init__(self, value): + super(VmUsageBarWidget.VmUsageBarItem, self).__init__() + self.value = value + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + return self.value < other.value + + def __init__(self, min, max, format, update_func, vm, load, hue=210, parent = None): + super (VmUsageBarWidget, self).__init__(parent) + + + self.min = min + self.max = max + self.update_func = update_func + self.value = min + + self.widget = QProgressBar() + self.widget.setMinimum(min) + self.widget.setMaximum(max) + self.widget.setFormat(format); + + self.widget.setStyleSheet( + "QProgressBar:horizontal{" +\ + "border: 1px solid hsv({0}, 100, 250);".format(hue) +\ + "border-radius: 4px;\ + background: white;\ + text-align: center;\ + }\ + QProgressBar::chunk:horizontal {\ + background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, " +\ + "stop: 0 hsv({0}, 170, 207),".format(hue) + + " stop: 1 white); \ + }" + ) + + layout = QHBoxLayout() + layout.addWidget(self.widget) + + self.setLayout(layout) + self.tableItem = self.VmUsageBarItem(min) + + self.update_load(vm, load) + + + + def update_load(self, vm, load): + self.value = self.update_func(vm, load) + self.widget.setValue(self.value) + self.tableItem.set_value(self.value) + +class ChartWidget (QWidget): + + class ChartItem (QTableWidgetItem): + def __init__(self, value): + super(ChartWidget.ChartItem, self).__init__() + self.value = value + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + return self.value < other.value + + def __init__(self, vm, update_func, hue, load = 0, parent = None): + super (ChartWidget, self).__init__(parent) + self.update_func = update_func + self.hue = hue + if hue < 0 or hue > 255: + self.hue = 255 + self.load = load + assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) + self.load_history = [self.load] + self.tableItem = ChartWidget.ChartItem(self.load) + + def update_load (self, vm, load): + self.load = self.update_func(vm, load) + + assert self.load >= 0, "load = {0}".format(self.load) + # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) + if self.load > 100: + # FIXME: This is an ugly workaround for cpu_load:/ + self.load = 100 + + self.load_history.append (self.load) + self.tableItem.set_value(self.load) + self.repaint() + + def paintEvent (self, Event = None): + p = QPainter (self) + dx = 4 + + W = self.width() + H = self.height() - 5 + N = len(self.load_history) + if N > W/dx: + tail = N - W/dx + N = W/dx + self.load_history = self.load_history[tail:] + + assert len(self.load_history) == N + + for i in range (0, N-1): + val = self.load_history[N- i - 1] + sat = 70 + val*(255-70)/100 + color = QColor.fromHsv (self.hue, sat, 255) + pen = QPen (color) + pen.setWidth(dx-1) + p.setPen(pen) + if val > 0: + p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100) + + + +class VmUpdateInfoWidget(QWidget): + + class VmUpdateInfoItem (QTableWidgetItem): + def __init__(self, value): + super(VmUpdateInfoWidget.VmUpdateInfoItem, self).__init__() + self.value = value + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + if self.value == "outdated": + return other.value == "outdated" + elif self.value == "update": + return other.value == "outdated" or other.value == "update" + elif self.value == "ok": + return other.value == "outdated" or other.value == "update" or other.value == "ok" + else: + return True + + def __init__(self, vm, show_text=True, parent = None): + super (VmUpdateInfoWidget, self).__init__(parent) + layout = QHBoxLayout () + self.show_text = show_text + if self.show_text: + self.label=QLabel("") + layout.addWidget(self.label, alignment=Qt.AlignCenter) + else: + self.icon = QLabel("") + layout.addWidget(self.icon, alignment=Qt.AlignHCenter) + self.setLayout(layout) + + self.previous_outdated = False + self.previous_update_recommended = None + self.value = None + self.tableItem = VmUpdateInfoWidget.VmUpdateInfoItem(self.value) def update_outdated(self, vm): outdated = vm.is_outdated() - if outdated != self.previous_outdated: - if outdated: - self.label_name.setText(vm.name + " (outdated)") - else: - self.label_name.setText(vm.name) + if outdated and not self.previous_outdated: + self.update_status_widget("outdated") + self.previous_outdated = outdated if vm.is_updateable(): update_recommended = self.previous_update_recommended @@ -179,143 +370,45 @@ class VmInfoWidget (QWidget): update_recommended = True else: update_recommended = False - if update_recommended != self.previous_update_recommended: - if update_recommended: - self.label_name.setText(vm.name + " (check updates)") - else: - self.label_name.setText(vm.name) - self.previous_update_recommended = update_recommended + if not self.show_text and self.previous_update_recommended != False: + self.update_status_widget("ok") + + if update_recommended and not self.previous_update_recommended: + self.update_status_widget("update") + self.previous_update_recommended = update_recommended -class VmUsageWidget (QWidget): - def __init__(self, vm, cpu_load = 0, parent = None): - super (VmUsageWidget, self).__init__(parent) + def update_status_widget(self, state): + self.value = state + self.tableItem.set_value(state) + if state == "ok": + label_text = "" + icon_path = ":/flag-green.png" + tooltip_text = "VM up to date" + elif state == "update": + label_text = "Check updates" + icon_path = ":/flag-yellow.png" + tooltip_text = "Update recommended" + elif state == "outdated": + label_text = "VM outdated" + icon_path = ":/flag-red.png" + tooltip_text = "VM outdated" - self.cpu_widget = QProgressBar() - self.mem_widget = QProgressBar() - self.cpu_widget.setMinimum(0) - self.cpu_widget.setMaximum(100) - self.mem_widget.setMinimum(0) - self.mem_widget.setMaximum(qubes_host.memory_total/1024) - self.mem_widget.setFormat ("%v MB"); - self.cpu_label = QLabel("CPU") - self.mem_label = QLabel("MEM") - - layout_cpu = QHBoxLayout() - layout_cpu.addWidget(self.cpu_label) - layout_cpu.addWidget(self.cpu_widget) - - layout_mem = QHBoxLayout() - layout_mem.addWidget(self.mem_label) - layout_mem.addWidget(self.mem_widget) - - layout = QVBoxLayout() - layout.addLayout(layout_cpu) - layout.addLayout(layout_mem) - - self.setLayout(layout) - - self.update_load(vm, cpu_load) - - def update_load(self, vm, cpu_load): - self.cpu_load = cpu_load if vm.last_power_state else 0 - self.mem_load = vm.get_mem()/1024 if vm.last_power_state else 0 - - self.cpu_widget.setValue(self.cpu_load) - self.mem_widget.setValue(self.mem_load) - - def resizeEvent(self, Event = None): - label_width = max(self.mem_label.width(), self.cpu_label.width()) - self.mem_label.setMinimumWidth(label_width) - self.cpu_label.setMinimumWidth(label_width) - super (VmUsageWidget, self).resizeEvent(Event) - -class LoadChartWidget (QWidget): - - def __init__(self, vm, cpu_load = 0, parent = None): - super (LoadChartWidget, self).__init__(parent) - self.load = cpu_load if vm.last_power_state else 0 - assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - self.load_history = [self.load] - - def update_load (self, vm, cpu_load): - self.load = cpu_load if vm.last_power_state else 0 - assert self.load >= 0, "load = {0}".format(self.load) - # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - if self.load > 100: - # FIXME: This is an ugly workaround :/ - self.load = 100 - - self.load_history.append (self.load) - self.repaint() - - def paintEvent (self, Event = None): - p = QPainter (self) - dx = 4 - - W = self.width() - H = self.height() - 5 - N = len(self.load_history) - if N > W/dx: - tail = N - W/dx - N = W/dx - self.load_history = self.load_history[tail:] - - assert len(self.load_history) == N - - for i in range (0, N-1): - val = self.load_history[N- i - 1] - hue = 200 - sat = 70 + val*(255-70)/100 - color = QColor.fromHsv (hue, sat, 255) - pen = QPen (color) - pen.setWidth(dx-1) - p.setPen(pen) - if val > 0: - p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100) - -class MemChartWidget (QWidget): - - def __init__(self, vm, parent = None): - super (MemChartWidget, self).__init__(parent) - self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0 - assert self.load >= 0 and self.load <= 100, "mem = {0}".format(self.load) - self.load_history = [self.load] - - def update_load (self, vm): - self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0 - assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - self.load_history.append (self.load) - self.repaint() - - def paintEvent (self, Event = None): - p = QPainter (self) - dx = 4 - - W = self.width() - H = self.height() - 5 - N = len(self.load_history) - if N > W/dx: - tail = N - W/dx - N = W/dx - self.load_history = self.load_history[tail:] - - assert len(self.load_history) == N - - for i in range (0, N-1): - val = self.load_history[N- i - 1] - hue = 120 - sat = 70 + val*(255-70)/100 - color = QColor.fromHsv (hue, sat, 255) - pen = QPen (color) - pen.setWidth(dx-1) - p.setPen(pen) - if val > 0: - p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100) + if self.show_text: + self.label.setText(label_text) + else: + self.layout().removeWidget(self.icon) + self.icon.deleteLater() + self.icon = VmIconWidget(icon_path, True) + self.icon.setToolTip(tooltip_text) + self.layout().addWidget(self.icon, alignment=Qt.AlignCenter) class VmRowInTable(object): - def __init__(self, vm, row_no, table): + cpu_graph_hue = 210 + mem_graph_hue = 120 + + def __init__(self, vm, row_no, table, block_manager): self.vm = vm self.row_no = row_no @@ -323,24 +416,46 @@ class VmRowInTable(object): self.info_widget = VmInfoWidget(vm) table.setCellWidget(row_no, 0, self.info_widget) + table.setItem(row_no, 0, self.info_widget.tableItem) - self.usage_widget = VmUsageWidget(vm) - table.setCellWidget(row_no, 1, self.usage_widget) + self.upd_widget = VmUpdateInfoWidget(vm, False) + table.setCellWidget(row_no, 1, self.upd_widget) + table.setItem(row_no, 1, self.upd_widget.tableItem) - self.load_widget = LoadChartWidget(vm) - table.setCellWidget(row_no, 2, self.load_widget) + self.template_widget = VmTemplateItem(vm) + table.setItem(row_no, 2, self.template_widget) + + self.netvm_widget = VmNetvmItem(vm) + table.setItem(row_no, 3, self.netvm_widget) - self.mem_widget = MemChartWidget(vm) - table.setCellWidget(row_no, 3, self.mem_widget) + self.cpu_usage_widget = VmUsageBarWidget(0, 100, "%v %", + lambda vm, val: val if vm.last_power_state else 0, vm, 0, self.cpu_graph_hue) + table.setCellWidget(row_no, 4, self.cpu_usage_widget) + table.setItem(row_no, 4, self.cpu_usage_widget.tableItem) + + self.load_widget = ChartWidget(vm, lambda vm, val: val if vm.last_power_state else 0, self.cpu_graph_hue, 0 ) + table.setCellWidget(row_no, 5, self.load_widget) + table.setItem(row_no, 5, self.load_widget.tableItem) + + self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB", + lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0, self.mem_graph_hue) + table.setCellWidget(row_no, 6, self.mem_usage_widget) + table.setItem(row_no, 6, self.mem_usage_widget.tableItem) - def update(self, counter, cpu_load = None): - self.info_widget.update_vm_state(self.vm) + self.mem_widget = ChartWidget(vm, lambda vm, val: vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0, self.mem_graph_hue, 0) + table.setCellWidget(row_no, 7, self.mem_widget) + table.setItem(row_no, 7, self.mem_widget.tableItem) + + + def update(self, counter, blk_visible = None, cpu_load = None): + self.info_widget.update_vm_state(self.vm, blk_visible) if cpu_load is not None: - self.usage_widget.update_load(self.vm, cpu_load) + self.cpu_usage_widget.update_load(self.vm, cpu_load) + self.mem_usage_widget.update_load(self.vm, None) self.load_widget.update_load(self.vm, cpu_load) - self.mem_widget.update_load(self.vm) - self.info_widget.update_outdated(self.vm) + self.mem_widget.update_load(self.vm, None) + self.upd_widget.update_outdated(self.vm) class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg): def __init__(self, parent = None): @@ -370,117 +485,71 @@ class VmShutdownMonitor(QObject): else: QTimer.singleShot (vm_shutdown_timeout, self.check_if_vm_has_shutdown) -class ThreadMonitor(QObject): - def __init__(self): - self.success = True - self.error_msg = None - self.event_finished = threading.Event() - def set_error_msg(self, error_msg): - self.success = False - self.error_msg = error_msg - self.set_finished() - - def is_finished(self): - return self.event_finished.is_set() - - def set_finished(self): - self.event_finished.set() - - -class VmManagerWindow(QMainWindow): - columns_widths = [250, 200, 150, 150] - row_height = 50 - max_visible_rows = 14 +class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): + row_height = 30 + column_width = 200 + max_visible_rows = 7 update_interval = 1000 # in msec show_inactive_vms = True - columns_states = { 0: [0, 1], 1: [0, 2, 3] } + columns_indices = { "Name": 0, + "Upd": 1, + "Template": 2, + "NetVM": 3, + "CPU": 4, + "CPU Graph": 5, + "MEM": 6, + "MEM Graph": 7,} + + def __init__(self, parent=None): - super(VmManagerWindow, self).__init__(parent) - - - self.action_createvm = self.createAction ("Create AppVM", slot=self.create_appvm, - icon="createvm", tip="Create a new AppVM") - - self.action_removevm = self.createAction ("Remove AppVM", slot=self.remove_appvm, - icon="removevm", tip="Remove an existing AppVM (must be stopped first)") - - self.action_resumevm = self.createAction ("Start/Resume VM", slot=self.resume_vm, - icon="resumevm", tip="Start/Resume a VM") - - self.action_pausevm = self.createAction ("Pause VM", slot=self.pause_vm, - icon="pausevm", tip="Pause a running VM") - - self.action_shutdownvm = self.createAction ("Shutdown VM", slot=self.shutdown_vm, - icon="shutdownvm", tip="Shutdown a running VM") - - self.action_appmenus = self.createAction ("Select VM applications", slot=self.appmenus_select, - icon="root", tip="Select applications present in menu for this VM") - - self.action_updatevm = self.createAction ("Update VM", slot=self.update_vm, - icon="updateable", tip="Update VM system") - - self.action_showallvms = self.createAction ("Show/Hide Inactive VMs", slot=self.toggle_inactive_view, checkable=True, - icon="showallvms", tip="Show/Hide Inactive VMs") - - self.action_showcpuload = self.createAction ("Show/Hide CPU Load chart", slot=self.showcpuload, checkable=True, - icon="showcpuload", tip="Show/Hide CPU Load chart") - - self.action_editfwrules = self.createAction ("Edit VM Firewall rules", slot=self.edit_fw_rules, - icon="firewall", tip="Edit VM Firewall rules") - - - self.action_removevm.setDisabled(True) - self.action_resumevm.setDisabled(True) - self.action_pausevm.setDisabled(True) - self.action_shutdownvm.setDisabled(True) - self.action_appmenus.setDisabled(True) - self.action_updatevm.setDisabled(True) - - self.action_showallvms.setChecked(self.show_inactive_vms) - - self.toolbar = self.addToolBar ("Toolbar") - self.toolbar.setFloatable(False) - self.addActions (self.toolbar, (self.action_createvm, self.action_removevm, - None, - self.action_resumevm, self.action_shutdownvm, - self.action_editfwrules, self.action_appmenus, - self.action_updatevm, - None, - self.action_showcpuload, - self.action_showallvms, - )) - - self.table = QTableWidget() - self.setCentralWidget(self.table) - self.table.clear() - self.table.setColumnCount(len(VmManagerWindow.columns_widths)) - for (col, width) in enumerate (VmManagerWindow.columns_widths): - self.table.setColumnWidth (col, width) - - self.table.horizontalHeader().setResizeMode(QHeaderView.Stretch) - self.table.horizontalHeader().setResizeMode(0, QHeaderView.Fixed) - self.table.setAlternatingRowColors(True) - self.table.verticalHeader().hide() - self.table.horizontalHeader().hide() - self.table.setGridStyle(Qt.NoPen) - self.table.setSortingEnabled(False) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setSelectionMode(QTableWidget.SingleSelection) - - self.__cpugraphs = self.action_showcpuload.isChecked() - self.update_table_columns() - + super(VmManagerWindow, self).__init__() + self.setupUi(self) + self.toolbar = self.toolBar + self.qvm_collection = QubesVmCollection() - self.setWindowTitle("Qubes VM Manager") - + self.blk_manager = QubesBlockDevicesManager(self.qvm_collection) + self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed) - + cur_pos = self.pos() - self.setFixedWidth (self.get_minimum_table_width()) + self.table.setColumnWidth(0, self.column_width) + self.setSizeIncrement(QtCore.QSize(200, 30)) + self.centralwidget.setSizeIncrement(QtCore.QSize(200, 30)) + self.table.setSizeIncrement(QtCore.QSize(200, 30)) self.fill_table() self.move(cur_pos) + + self.table.setColumnHidden( self.columns_indices["NetVM"], True) + self.actionNetVM.setChecked(False) + self.table.setColumnHidden( self.columns_indices["CPU Graph"], True) + self.actionCPU_Graph.setChecked(False) + self.table.setColumnHidden( self.columns_indices["MEM Graph"], True) + self.actionMEM_Graph.setChecked(False) + self.table.setColumnWidth(self.columns_indices["Upd"], 50) + + self.table.sortItems(self.columns_indices["MEM"], Qt.DescendingOrder) + + self.context_menu = QMenu(self) + self.context_menu.addAction(self.action_settings) + self.context_menu.addAction(self.action_removevm) + self.context_menu.addAction(self.action_resumevm) + self.context_menu.addAction(self.action_pausevm) + self.context_menu.addAction(self.action_shutdownvm) + self.context_menu.addAction(self.action_appmenus) + self.context_menu.addAction(self.action_editfwrules) + self.context_menu.addAction(self.action_updatevm) + + self.blk_menu = QMenu("Block devices") + self.context_menu.addMenu(self.blk_menu) + + self.connect(self.table, SIGNAL("customContextMenuRequested(const QPoint&)"), self.open_context_menu) + self.connect(self.blk_menu, SIGNAL("triggered(QAction *)"), self.attach_dettach_device_triggered) + + self.table.setContentsMargins(0,0,0,0) + self.centralwidget.layout().setContentsMargins(0,0,0,0) + self.layout().setContentsMargins(0,0,0,0) self.counter = 0 self.shutdown_monitor = {} @@ -488,45 +557,43 @@ class VmManagerWindow(QMainWindow): self.last_measure_time = time.time() QTimer.singleShot (self.update_interval, self.update_table) + def show(self): + super(VmManagerWindow, self).show() + self.set_table_geom_height() + self.update_table_columns() + def set_table_geom_height(self): - # TODO: '6' -- WTF?! - tbl_H = self.toolbar.height() + 6 + \ - self.table.horizontalHeader().height() + 6 + minH = self.table.horizontalHeader().height() +\ + 2*self.table.frameWidth() + #All this sizing is kind of magic, so change it only if you have to + #or if you know what you're doing :) + n = self.table.rowCount(); - if n > VmManagerWindow.max_visible_rows: - n = VmManagerWindow.max_visible_rows - for i in range (0, n): - tbl_H += self.table.rowHeight(i) - self.setFixedHeight(tbl_H) + maxH = minH + if n >= self.max_visible_rows: + minH += self.max_visible_rows*self.row_height + maxH += n*self.row_height + else: + minH += n*self.row_height + maxH = minH + + self.centralwidget.setMinimumHeight(minH) + self.centralwidget.setMaximumHeight(maxH) + mainwindow_to_add = self.menubar.height() +\ + self.toolbar.height() + \ + self.menubar.contentsMargins().top() + self.menubar.contentsMargins().bottom() +\ + self.toolbar.contentsMargins().top() + self.toolbar.contentsMargins().bottom() - def addActions(self, target, actions): - for action in actions: - if action is None: - target.addSeparator() - else: - target.addAction(action) - - - def createAction(self, text, slot=None, shortcut=None, icon=None, - tip=None, checkable=False, signal="triggered()"): - action = QAction(text, self) - if icon is not None: - action.setIcon(QIcon(":/%s.png" % icon)) - if shortcut is not None: - action.setShortcut(shortcut) - if tip is not None: - action.setToolTip(tip) - action.setStatusTip(tip) - if slot is not None: - self.connect(action, SIGNAL(signal), slot) - if checkable: - action.setCheckable(True) - return action + maxH += mainwindow_to_add + minH += mainwindow_to_add + self.setMaximumHeight(maxH) + self.setMinimumHeight(minH) + def get_vms_list(self): self.qvm_collection.lock_db_for_reading() self.qvm_collection.load() @@ -559,11 +626,12 @@ class VmManagerWindow(QMainWindow): return vms_to_display def fill_table(self): - self.table.clear() + self.table.setSortingEnabled(False) + self.table.clearContents() vms_list = self.get_vms_list() self.table.setRowCount(len(vms_list)) - vms_in_table = [] + vms_in_table = {} row_no = 0 for vm in vms_list: @@ -571,15 +639,15 @@ class VmManagerWindow(QMainWindow): continue if vm.internal: continue - vm_row = VmRowInTable (vm, row_no, self.table) - vms_in_table.append (vm_row) + vm_row = VmRowInTable (vm, row_no, self.table, self.blk_manager) + vms_in_table[vm.qid] = vm_row row_no += 1 self.table.setRowCount(row_no) - self.set_table_geom_height() self.vms_list = vms_list self.vms_in_table = vms_in_table self.reload_table = False + self.table.setSortingEnabled(True) def mark_table_for_update(self): @@ -587,7 +655,7 @@ class VmManagerWindow(QMainWindow): # When calling update_table() directly, always use out_of_schedule=True! def update_table(self, out_of_schedule=False): - + update_devs = self.update_block_devices() or out_of_schedule if manager_window.isVisible(): some_vms_have_changed_power_state = False for vm in self.vms_list: @@ -598,69 +666,103 @@ class VmManagerWindow(QMainWindow): if self.reload_table or ((not self.show_inactive_vms) and some_vms_have_changed_power_state): self.fill_table() + update_devs=True + + blk_visible = None + rows_with_blk = None + if update_devs == True: + rows_with_blk = [] + for d in self.blk_manager.attached_devs: + rows_with_blk.append( self.blk_manager.attached_devs[d]['attached_to']['vm']) if self.counter % 3 == 0 or out_of_schedule: (self.last_measure_time, self.last_measure_results) = \ qubes_host.measure_cpu_usage(self.last_measure_results, self.last_measure_time) - for vm_row in self.vms_in_table: + for vm_row in self.vms_in_table.values(): cur_cpu_load = None if vm_row.vm.get_xid() in self.last_measure_results: cur_cpu_load = self.last_measure_results[vm_row.vm.xid]['cpu_usage'] else: cur_cpu_load = 0 - vm_row.update(self.counter, cpu_load = cur_cpu_load) - else: - for vm_row in self.vms_in_table: - vm_row.update(self.counter) - self.table_selection_changed() + if rows_with_blk != None: + if vm_row.vm.name in rows_with_blk: + blk_visible = True + else: + blk_visible = False + + vm_row.update(self.counter, blk_visible=blk_visible, cpu_load = cur_cpu_load) + else: + for vm_row in self.vms_in_table.values(): + if rows_with_blk != None: + if vm_row.vm.name in rows_with_blk: + blk_visible = True + else: + blk_visible = False + + vm_row.update(self.counter, blk_visible=blk_visible) + + #self.table_selection_changed() if not out_of_schedule: self.counter += 1 QTimer.singleShot (self.update_interval, self.update_table) def update_table_columns(self): - state = 1 if self.__cpugraphs else 0 - columns = self.columns_states[state] - for i in range(0, self.table.columnCount()): - enabled = columns.count(i) > 0 - self.table.setColumnHidden(i, not enabled) + table_width = self.table.horizontalHeader().length() +\ + self.table.verticalScrollBar().width() + \ + 2*self.table.frameWidth() + 1 - self.setMinimumWidth(self.get_minimum_table_width()) + self.table.setFixedWidth( table_width ) + self.centralwidget.setFixedWidth(table_width) + self.setFixedWidth(table_width) + + def update_block_devices(self): + res, msg = self.blk_manager.update() + if msg != None and len(msg) > 0: + str = "\n".join(msg) + trayIcon.showMessage ("Qubes Manager", str, msecs=5000) + return res def table_selection_changed (self): + vm = self.get_selected_vm() - # Update available actions: + if vm != None: + # Update available actions: + self.action_settings.setEnabled(True) + self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state) + self.action_resumevm.setEnabled(not vm.last_power_state) + self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0) + self.action_shutdownvm.setEnabled(not vm.is_netvm() and vm.last_power_state and vm.qid != 0) + self.action_appmenus.setEnabled(not vm.is_netvm()) + self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm())) + self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0) + else: + self.action_settings.setEnabled(False) + self.action_removevm.setEnabled(False) + self.action_resumevm.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_shutdownvm.setEnabled(False) + self.action_appmenus.setEnabled(False) + self.action_editfwrules.setEnabled(False) + self.action_updatevm.setEnabled(False) - self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state) - self.action_resumevm.setEnabled(not vm.last_power_state) - self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0) - self.action_shutdownvm.setEnabled(not vm.is_netvm() and vm.last_power_state and vm.qid != 0) - self.action_appmenus.setEnabled(not vm.is_netvm()) - self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm())) - self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0) - def get_minimum_table_width(self): - tbl_W = 0 - for (col, w) in enumerate(VmManagerWindow.columns_widths): - if not self.table.isColumnHidden(col): - tbl_W += w - - return tbl_W def closeEvent (self, event): if event.spontaneous(): # There is something borked in Qt, as the logic here is inverted on X11 self.hide() event.ignore() - def create_appvm(self): + + @pyqtSlot(name='on_action_createvm_triggered') + def action_createvm_triggered(self): dialog = NewAppVmDlg() - # Theoretically we should be locking for writing here and unlock # only after the VM creation finished. But the code would be more messy... # Instead we lock for writing in the actual worker thread @@ -746,12 +848,19 @@ class VmManagerWindow(QMainWindow): def get_selected_vm(self): + #vm selection relies on the VmInfo widget's value used for sorting by VM name row_index = self.table.currentRow() - assert self.vms_in_table[row_index] is not None - vm = self.vms_in_table[row_index].vm - return vm + if row_index != None: + (vm_name, qid) = self.table.item(row_index, self.columns_indices["Name"]).value + assert self.vms_in_table[qid] is not None + vm = self.vms_in_table[qid].vm + return vm + else: + return None + + @pyqtSlot(name='on_action_removevm_triggered') + def action_removevm_triggered(self): - def remove_appvm(self): vm = self.get_selected_vm() assert not vm.is_running() assert not vm.installed_by_rpm @@ -820,7 +929,8 @@ class VmManagerWindow(QMainWindow): thread_monitor.set_finished() - def resume_vm(self): + @pyqtSlot(name='on_action_resumevm_triggered') + def action_resumevm_triggered(self): vm = self.get_selected_vm() assert not vm.is_running() @@ -862,7 +972,8 @@ class VmManagerWindow(QMainWindow): thread_monitor.set_finished() - def pause_vm(self): + @pyqtSlot(name='on_action_pausevm_triggered') + def action_pausevm_triggered(self): vm = self.get_selected_vm() assert vm.is_running() try: @@ -871,7 +982,8 @@ class VmManagerWindow(QMainWindow): QMessageBox.warning (None, "Error pausing VM!", "ERROR: {0}".format(ex)) return - def shutdown_vm(self): + @pyqtSlot(name='on_action_shutdownvm_triggered') + def action_shutdownvm_triggered(self): vm = self.get_selected_vm() assert vm.is_running() @@ -893,12 +1005,22 @@ class VmManagerWindow(QMainWindow): self.shutdown_monitor[vm.qid] = VmShutdownMonitor (vm) QTimer.singleShot (vm_shutdown_timeout, self.shutdown_monitor[vm.qid].check_if_vm_has_shutdown) - def appmenus_select(self): + @pyqtSlot(name='on_action_settings_triggered') + def action_settings_triggered(self): vm = self.get_selected_vm() - select_window = AppmenuSelectWindow(vm) - select_window.exec_() + settings_window = VMSettingsWindow(vm, app, self.qvm_collection, "basic") + settings_window.exec_() + - def update_vm(self): + @pyqtSlot(name='on_action_appmenus_triggered') + def action_appmenus_triggered(self): + vm = self.get_selected_vm() + settings_window = VMSettingsWindow(vm, app, self.qvm_collection, "applications") + settings_window.exec_() + + + @pyqtSlot(name='on_action_updatevm_triggered') + def action_updatevm_triggered(self): vm = self.get_selected_vm() if not vm.is_running(): @@ -939,28 +1061,186 @@ class VmManagerWindow(QMainWindow): return thread_monitor.set_finished() - def showcpuload(self): - self.__cpugraphs = self.action_showcpuload.isChecked() - self.update_table_columns() - - def toggle_inactive_view(self): + @pyqtSlot(name='on_action_showallvms_triggered') + def action_showallvms_triggered(self): self.show_inactive_vms = self.action_showallvms.isChecked() self.mark_table_for_update() self.update_table(out_of_schedule = True) + self.set_table_geom_height() - def edit_fw_rules(self): + @pyqtSlot(name='on_action_editfwrules_triggered') + def action_editfwrules_triggered(self): vm = self.get_selected_vm() - dialog = EditFwRulesDlg() - model = QubesFirewallRulesModel() - model.set_vm(vm) - dialog.set_model(model) + settings_window = VMSettingsWindow(vm, app, self.qvm_collection, "firewall") + settings_window.exec_() + + @pyqtSlot(name='on_action_global_settings_triggered') + def action_global_settings_triggered(self): + global_settings_window = GlobalSettingsWindow() + global_settings_window.exec_() + + + @pyqtSlot(name='on_action_restore_triggered') + def action_restore_triggered(self): + restore_window = RestoreVMsWindow(app, self.qvm_collection, self.blk_manager) + restore_window.exec_() + + @pyqtSlot(name='on_action_backup_triggered') + def action_backup_triggered(self): + backup_window = BackupVMsWindow(app, self.qvm_collection, self.blk_manager) + backup_window.exec_() + + + + def showhide_collumn(self, col_num, show): + self.table.setColumnHidden( col_num, not show) + self.update_table_columns() + + def on_actionUpd_toggled(self, checked): + self.showhide_collumn( self.columns_indices['Upd'], checked) + + def on_actionTemplate_toggled(self, checked): + self.showhide_collumn( self.columns_indices['Template'], checked) + + def on_actionNetVM_toggled(self, checked): + self.showhide_collumn( self.columns_indices['NetVM'], checked) + + def on_actionCPU_toggled(self, checked): + self.showhide_collumn( self.columns_indices['CPU'], checked) + + def on_actionCPU_Graph_toggled(self, checked): + self.showhide_collumn( self.columns_indices['CPU Graph'], checked) + + def on_actionMEM_toggled(self, checked): + self.showhide_collumn( self.columns_indices['MEM'], checked) + + def on_actionMEM_Graph_toggled(self, checked): + self.showhide_collumn( self.columns_indices['MEM Graph'], checked) + + + @pyqtSlot('const QPoint&') + def open_context_menu(self, point): + vm = self.get_selected_vm() + if not vm.is_running(): + self.blk_menu.setEnabled(False) + else: + self.blk_menu.clear() + self.blk_menu.setEnabled(True) + if len(self.blk_manager.attached_devs) > 0 : + for d in self.blk_manager.attached_devs: + if self.blk_manager.attached_devs[d]['attached_to']['vm'] == vm.name: + str = "Detach " + d + " " + unicode(self.blk_manager.attached_devs[d]['size']) + " " + self.blk_manager.attached_devs[d]['desc'] + action = self.blk_menu.addAction(QIcon(":/remove.png"), str) + action.setData(QVariant(d)) + + if len(self.blk_manager.free_devs) > 0: + for d in self.blk_manager.free_devs: + if d.startswith(vm.name): + continue + str = "Attach " + d + " " + unicode(self.blk_manager.free_devs[d]['size']) + " " + self.blk_manager.free_devs[d]['desc'] + action = self.blk_menu.addAction(QIcon(":/add.png"), str) + action.setData(QVariant(d)) + + if self.blk_menu.isEmpty(): + self.blk_menu.setEnabled(False) + + self.context_menu.exec_(self.table.mapToGlobal(point)) + + @pyqtSlot('QAction *') + def attach_dettach_device_triggered(self, action): + dev = str(action.data().toString()) + vm = self.get_selected_vm() + if dev in self.blk_manager.attached_devs: + self.blk_manager.detach_device(vm, dev) + else: + self.blk_manager.attach_device(vm, dev) + + +class QubesBlockDevicesManager(): + def __init__(self, qvm_collection): + self.qvm_collection = qvm_collection + self.attached_devs = {} + self.free_devs = {} + + self.current_blk = {} + self.current_attached = {} + self.devs_changed = False + + def update(self): + blk = qubesutils.block_list() + msg = [] + for b in blk: + att = qubesutils.block_check_attached(None, blk[b]['device'], backend_xid = blk[b]['xid']) + if b in self.current_blk: + if blk[b] == self.current_blk[b]: + if self.current_attached[b] != att: #devices the same, sth with attaching changed + self.current_attached[b] = att + self.devs_changed = True + else: #device changed ?! + self.current_blk[b] = blk[b] + self.current_attached[b] = att + self.devs_changed = True + else: #new device + self.current_blk[b] = blk[b] + self.current_attached[b] = att + self.devs_changed = True + msg.append("Attached new device: {0}".format(blk[b]['device'])) + + to_delete = [] + for b in self.current_blk: #remove devices that are not there anymore + if b not in blk: + to_delete.append(b) + self.devs_changed = True + msg.append("Detached device: {0}".format(self.current_blk[b]['device'])) + + for d in to_delete: + del self.current_blk[d] + del self.current_attached[d] + + if self.devs_changed == True: + self.devs_changed = False + self.__update_blk_entries__() + return True, msg + else: + return False, None + + + def __update_blk_entries__(self): + self.free_devs.clear() + self.attached_devs.clear() + + for b in self.current_attached: + if self.current_attached[b]: + self.attached_devs[b] = self.__make_entry__(b, self.current_blk[b], self.current_attached[b]) + else: + self.free_devs[b] = self.__make_entry__(b, self.current_blk[b], None) + + def __make_entry__(self, k, dev, att): + size_str = qubesutils.bytes_to_kmg(dev['size']) + entry = { 'dev': dev['device'], + 'backend_name': dev['vm'], + 'desc': dev['desc'], + 'size': size_str, + 'attached_to': att, } + return entry + + def attach_device(self, vm, dev): + backend_vm_name = self.free_devs[dev]['backend_name'] + dev_id = self.free_devs[dev]['dev'] + backend_vm = self.qvm_collection.get_vm_by_name(backend_vm_name) + trayIcon.showMessage ("Qubes Manager", "{0} - attaching {1}".format(vm.name, dev), msecs=3000) + qubesutils.block_attach(vm, backend_vm, dev_id) + self.devs_changed = True + + def detach_device(self, vm, dev_name): + dev_id = self.attached_devs[dev_name]['attached_to']['devid'] + vm_xid = self.attached_devs[dev_name]['attached_to']['xid'] + trayIcon.showMessage ("Qubes Manager", "{0} - detaching {1}".format(vm.name, dev_name), msecs=3000) + qubesutils.block_detach(None, dev_id, vm_xid) + self.devs_changed = True + - if vm.netvm_vm is not None and not vm.netvm_vm.is_proxyvm(): - QMessageBox.warning (None, "VM configuration problem!", "The '{0}' AppVM is not network connected to a FirewallVM!

".format(vm.name) +\ - "You may edit the '{0}' VM firewall rules, but these will not take any effect until you connect it to a working Firewall VM.".format(vm.name)) - if dialog.exec_(): - model.apply_rules() class QubesTrayIcon(QSystemTrayIcon): def __init__(self, icon): @@ -1095,4 +1375,3 @@ def main(): app.exec_() trayIcon = None - diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py new file mode 100644 index 0000000..a9de974 --- /dev/null +++ b/qubesmanager/multiselectwidget.py @@ -0,0 +1,52 @@ +import sys +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from ui_multiselectwidget import * + +class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): + + __pyqtSignals__ = ("selected_changed()",) + + def __init__(self, parent=None): + super(MultiSelectWidget, self).__init__() + self.setupUi(self); + self.add_selected_button.clicked.connect(self.add_selected) + self.add_all_button.clicked.connect(self.add_all) + self.remove_selected_button.clicked.connect(self.remove_selected) + self.remove_all_button.clicked.connect(self.remove_all) + self.available_list.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.selected_list.setSelectionMode(QAbstractItemView.ExtendedSelection) + + def switch_selected(self, src, dst): + selected = src.selectedItems() + + for s in selected: + row = src.indexFromItem(s).row() + item = src.takeItem(row) + dst.addItem(item) + dst.sortItems() + self.emit(SIGNAL("selected_changed()")) + + def add_selected(self): + self.switch_selected(self.available_list, self.selected_list) + + def remove_selected(self): + self.switch_selected(self.selected_list, self.available_list) + + def move_all(self, src, dst): + while src.count() > 0: + item = src.takeItem(0) + dst.addItem(item) + dst.sortItems() + self.emit(SIGNAL("selected_changed()")) + + def add_all(self): + self.move_all(self.available_list, self.selected_list) + + def remove_all(self): + self.move_all(self.selected_list, self.available_list) + + def clear(self): + self.available_list.clear() + self.selected_list.clear() + diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py new file mode 100644 index 0000000..a2d509e --- /dev/null +++ b/qubesmanager/restore.py @@ -0,0 +1,276 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# 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 sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost +from qubes.qubes import qubes_base_dir +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +from operator import itemgetter +from thread_monitor import * + +from qubes import qubesutils + +from ui_restoredlg import * +from multiselectwidget import * + +from backup_utils import * + + + +class RestoreVMsWindow(Ui_Restore, QWizard): + + __pyqtSignals__ = ("restore_progress(int)",) + + def __init__(self, app, qvm_collection, blk_manager, parent=None): + super(RestoreVMsWindow, self).__init__(parent) + + self.app = app + self.qvm_collection = qvm_collection + self.blk_manager = blk_manager + + self.dev_mount_path = None + self.backup_dir = None + self.restore_options = None + self.backup_vms_list = None + self.func_output = [] + + self.excluded = {} + + for vm in self.qvm_collection.values(): + if vm.qid == 0: + self.vm = vm + break; + + assert self.vm != None + + self.setupUi(self) + + self.select_vms_widget = MultiSelectWidget(self) + self.select_vms_layout.insertWidget(1, self.select_vms_widget) + + self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) + self.connect(self.dev_combobox, SIGNAL("activated(int)"), self.dev_combobox_activated) + self.connect(self, SIGNAL("restore_progress(QString)"), self.commit_text_edit.append) + + self.select_dir_page.isComplete = self.has_selected_dir + self.select_vms_page.isComplete = self.has_selected_vms + #FIXME + #this causes to run isComplete() twice, I don't know why + self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) + + fill_devs_list(self) + self.__init_restore_options__() + + + def dev_combobox_activated(self, idx): + dev_combobox_activated(self, idx) + + + @pyqtSlot(name='on_select_path_button_clicked') + def select_path_button_clicked(self): + select_path_button_clicked(self) + + def on_ignore_missing_toggled(self, checked): + self.restore_options['use-default-template'] = checked + self.restore_options['use-default-netvm'] = checked + + def on_ignore_uname_mismatch_toggled(self, checked): + self.restore_options['ignore-username-mismatch'] = checked + + def on_skip_dom0_toggled(self, checked): + self.restore_options['dom0-home'] = checked + + + def __fill_vms_list__(self): + if self.backup_vms_list != None: + return + + self.select_vms_widget.selected_list.clear() + self.select_vms_widget.available_list.clear() + + self.vms_to_restore = qubesutils.backup_restore_prepare(str(self.backup_dir), self.restore_options, self.qvm_collection) + for vmname in self.vms_to_restore: + self.select_vms_widget.available_list.addItem(vmname) + + def __init_restore_options__(self): + if not self.restore_options: + self.restore_options = {} + qubesutils.backup_restore_set_defaults(self.restore_options) + + if 'use-default-template' in self.restore_options and 'use-default-netvm' in self.restore_options: + val = self.restore_options['use-default-template'] and self.restore_options['use-default-netvm'] + self.ignore_missing.setChecked(val) + else: + self.ignore_missing.setChecked(False) + + if 'ignore-username-mismatch' in self.restore_options: + self.ignore_uname_mismatch.setChecked(self.restore_options['ignore-username-mismatch']) + + if 'dom0-home' in self.restore_options: + self.skip_dom0.setChecked(self.restore_options['dom0-home']) + + + + def gather_output(self, s): + self.func_output.append(s) + + def restore_error_output(self, s): + self.emit(SIGNAL("restore_progress(QString)"), '{0}'.format(s)) + + + def restore_output(self, s): + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format(s)) + + + def __do_restore__(self, thread_monitor): + err_msg = [] + self.qvm_collection.lock_db_for_writing() + try: + qubesutils.backup_restore_do(str(self.backup_dir), self.vms_to_restore, self.qvm_collection, self.restore_output, self.restore_error_output) + except Exception as ex: + err_msg.append(str(ex)) + + self.qvm_collection.unlock_db() + if len(err_msg) > 0 : + thread_monitor.set_error_msg('\n'.join(err_msg)) + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format("Finished with errors!")) + else: + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format("Finished successfully!")) + + thread_monitor.set_finished() + + + def current_page_changed(self, id): + + if self.currentPage() is self.select_vms_page: + self.__fill_vms_list__() + + elif self.currentPage() is self.confirm_page: + for v in self.excluded: + self.vms_to_restore[v] = self.excluded[v] + self.excluded = {} + for i in range(self.select_vms_widget.available_list.count()): + vmname = self.select_vms_widget.available_list.item(i).text() + self.excluded[str(vmname)] = self.vms_to_restore[str(vmname)] + del self.vms_to_restore[str(vmname)] + + del self.func_output[:] + qubesutils.backup_restore_print_summary(self.vms_to_restore, print_callback = self.gather_output) + self.confirm_text_edit.setReadOnly(True) + self.confirm_text_edit.setFontFamily("Monospace") + self.confirm_text_edit.setText("\n".join(self.func_output)) + + + elif self.currentPage() is self.commit_page: + self.button(self.CancelButton).setDisabled(True) + self.button(self.FinishButton).setDisabled(True) + + self.thread_monitor = ThreadMonitor() + thread = threading.Thread (target= self.__do_restore__ , args=(self.thread_monitor,)) + thread.daemon = True + thread.start() + + while not self.thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + #if not self.thread_monitor.success: + #QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) + + umount_device(self.dev_mount_path) + self.button(self.FinishButton).setEnabled(True) + + + + def has_selected_dir(self): + return self.backup_dir != None + + def has_selected_vms(self): + return self.select_vms_widget.selected_list.count() > 0 + + + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes Restore VMs application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes Restore VMs") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + global restore_window + restore_window = RestoreVMsWindow() + + restore_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py new file mode 100644 index 0000000..9ca2475 --- /dev/null +++ b/qubesmanager/settings.py @@ -0,0 +1,403 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# 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 sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesVmLabels +from qubes.qubes import QubesException +from qubes.qubes import qubes_appmenu_create_cmd +from qubes.qubes import qubes_appmenu_remove_cmd +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost +from qubes.qubes import qrexec_client_path + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +import threading +from operator import itemgetter + +from ui_settingsdlg import * +from multiselectwidget import * +from appmenu_select import * +from firewall import * + + +class VMSettingsWindow(Ui_SettingsDialog, QDialog): + tabs_indices = {"basic": 0, + "advanced": 1, + "firewall": 2, + "devices": 3, + "applications": 4, + "services": 5,} + + def __init__(self, vm, app, qvm_collection, init_page="basic", parent=None): + super(VMSettingsWindow, self).__init__(parent) + + self.app = app + self.qvm_collection = qvm_collection + self.vm = vm + if self.vm.template_vm: + self.source_vm = self.vm.template_vm + else: + self.source_vm = self.vm + + self.setupUi(self) + if init_page in self.tabs_indices: + idx = self.tabs_indices[init_page] + assert (idx in range(self.tabWidget.count())) + self.tabWidget.setCurrentIndex(idx) + + self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) + self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) + + self.tabWidget.currentChanged.connect(self.current_tab_changed) + + ###### basic tab + self.__init_basic_tab__() + + ###### firewall tab + + model = QubesFirewallRulesModel() + model.set_vm(vm) + self.set_fw_model(model) + + + self.newRuleButton.clicked.connect(self.new_rule_button_pressed) + self.editRuleButton.clicked.connect(self.edit_rule_button_pressed) + self.deleteRuleButton.clicked.connect(self.delete_rule_button_pressed) + self.policyAllowRadioButton.toggled.connect(self.policy_radio_toggled) + self.dnsCheckBox.toggled.connect(self.dns_checkbox_toggled) + self.icmpCheckBox.toggled.connect(self.icmp_checkbox_toggled) + + ####### devices tab + self.dev_list = MultiSelectWidget(self) + self.devices_layout.addWidget(self.dev_list) + + ####### apps tab + if not vm.is_netvm(): + self.app_list = MultiSelectWidget(self) + self.apps_layout.addWidget(self.app_list) + self.AppListManager = AppmenuSelectManager(self.vm, self.app_list) + else: + self.tabWidget.setTabEnabled(self.tabs_indices["applications"], False) + + def reject(self): + self.done(0) + + #needed not to close the dialog before applying changes + def accept(self): + pass + + def save_and_apply(self): + thread_monitor = ThreadMonitor() + thread = threading.Thread (target=self.__save_changes__, args=(thread_monitor,)) + thread.daemon = True + thread.start() + + progress = QProgressDialog ("Applying settings to {0}...".format(self.vm.name), "", 0, 0) + progress.setCancelButton(None) + progress.setModal(True) + progress.show() + + while not thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + progress.hide() + + if not thread_monitor.success: + QMessageBox.warning (None, "Error while changing settings for {0}!", "ERROR: {1}".format(self.vm.name, thread_monitor.error_msg)) + + self.done(0) + + def __save_changes__(self, thread_monitor): + ret = self.__apply_basic_tab__() + if len(ret) > 0 : + thread_monitor.set_error_msg('\n'.join(ret)) + thread_monitor.set_finished() + return + #self.fw_model.apply_rules() + self.AppListManager.save_appmenu_select_changes() + thread_monitor.set_finished() + + def current_tab_changed(self, idx): + if idx == self.tabs_indices["firewall"]: + if self.vm.netvm_vm is not None and not self.vm.netvm_vm.is_proxyvm(): + QMessageBox.warning (None, "VM configuration problem!", "The '{0}' AppVM is not network connected to a FirewallVM!

".format(self.vm.name) +\ + "You may edit the '{0}' VM firewall rules, but these will not take any effect until you connect it to a working Firewall VM.".format(self.vm.name)) + + + + ######### basic tab + + def __init_basic_tab__(self): + self.vmname.setText(self.vm.name) + + #self.qvm_collection.lock_db_for_reading() + #self.qvm_collection.load() + #self.qvm_collection.unlock_db() + + self.label_list = QubesVmLabels.values() + self.label_list.sort(key=lambda l: l.index) + self.label_idx = 0 + for (i, label) in enumerate(self.label_list): + if label == self.vm.label: + self.label_idx = i + self.vmlabel.insertItem(i, label.name) + self.vmlabel.setItemIcon (i, QIcon(label.icon_path)) + self.vmlabel.setCurrentIndex(self.label_idx) + + if not self.vm.is_template() and self.vm.template_vm is not None: + template_vm_list = [vm for vm in self.qvm_collection.values() if not vm.internal and vm.is_template()] + self.template_idx = 0 + for (i, vm) in enumerate(template_vm_list): + text = vm.name + if vm is self.qvm_collection.get_default_template_vm(): + text += " (default)" + if vm.qid == self.vm.template_vm.qid: + self.template_idx = i + text += " (current)" + self.template_name.insertItem(i, text) + self.template_name.setCurrentIndex(self.template_idx) + else: + self.template_name.setEnabled(False) + + if not self.vm.is_netvm(): + netvm_list = [vm for vm in self.qvm_collection.values() if not vm.internal and vm.is_netvm()] + self.netvm_idx = 0 + for (i, vm) in enumerate(netvm_list): + text = vm.name + if vm is self.qvm_collection.get_default_netvm_vm(): + text += " (default)" + if vm.qid == self.vm.netvm_vm.qid: + self.netvm_idx = i + text += " (current)" + self.netVM.insertItem(i, text) + self.netVM.setCurrentIndex(self.netvm_idx) + else: + self.netVM.setEnabled(False) + + #self.vmname.selectAll() + #self.vmname.setFocus() + + def __apply_basic_tab__(self): + msg = [] + + if self.vm.is_running(): + msg.append("Can't change settings of a running VM.") + msg.append("telemele") + return msg + + # vmname changed + vmname = str(self.vmname.text()) + if self.vm.name != vmname: + if self.qvm_collection.get_vm_by_name(vmname) is not None: + msg.append("A VM named {0} already exists in the system!".format(vmname)) + else: + oldname = self.vm.name + try: + self.qvm_collection.lock_db_for_writing() + self.vm.pre_rename(vmname) + self.vm.set_name(vmname) + self.vm.post_rename(oldname) + self.qvm_collection.save() + except Exception as ex: + msg.append(str(ex)) + finally: + self.qvm_collection.unlock_db() + + #vm label changed + if self.vmlabel.currentIndex() != self.label_idx: + label = self.label_list[self.vmlabel.currentIndex()] + self.qvm_collection.lock_db_for_writing() + self.vm.label = label + self.qvm_collection.save() + self.qvm_collection.unlock_db() + + return msg + + # template_vm = template_vm_list[dialog.template_name.currentIndex()] + # allow_networking = dialog.allow_networking.isChecked() + + ######### firewall tab related + + def set_fw_model(self, model): + self.fw_model = model + self.rulesTreeView.setModel(model) + self.rulesTreeView.header().setResizeMode(QHeaderView.ResizeToContents) + self.rulesTreeView.header().setResizeMode(0, QHeaderView.Stretch) + self.set_allow(model.allow) + self.dnsCheckBox.setChecked(model.allowDns) + self.icmpCheckBox.setChecked(model.allowIcmp) + + def set_allow(self, allow): + self.policyAllowRadioButton.setChecked(allow) + self.policyDenyRadioButton.setChecked(not allow) + + def policy_radio_toggled(self, on): + self.fw_model.allow = self.policyAllowRadioButton.isChecked() + + def dns_checkbox_toggled(self, on): + self.fw_model.allowDns = on + + def icmp_checkbox_toggled(self, on): + self.fw_model.allowIcmp = on + + def new_rule_button_pressed(self): + dialog = NewFwRuleDlg() + self.run_rule_dialog(dialog) + + def edit_rule_button_pressed(self): + dialog = NewFwRuleDlg() + dialog.set_ok_enabled(True) + selected = self.rulesTreeView.selectedIndexes() + if len(selected) > 0: + row = self.rulesTreeView.selectedIndexes().pop().row() + address = self.fw_model.get_column_string(0, row).replace(' ', '') + dialog.addressComboBox.setItemText(0, address) + dialog.addressComboBox.setCurrentIndex(0) + service = self.fw_model.get_column_string(1, row) + dialog.serviceComboBox.setItemText(0, service) + dialog.serviceComboBox.setCurrentIndex(0) + self.run_rule_dialog(dialog, row) + + def delete_rule_button_pressed(self): + for i in set([index.row() for index in self.rulesTreeView.selectedIndexes()]): + self.fw_model.removeChild(i) + + def run_rule_dialog(self, dialog, row = None): + if dialog.exec_(): + address = str(dialog.addressComboBox.currentText()) + service = str(dialog.serviceComboBox.currentText()) + port = None + port2 = None + + unmask = address.split("/", 1) + if len(unmask) == 2: + address = unmask[0] + netmask = int(unmask[1]) + else: + netmask = 32 + + if address == "*": + address = "0.0.0.0" + netmask = 0 + + if service == "*": + service = "0" + try: + range = service.split("-", 1) + if len(range) == 2: + port = int(range[0]) + port2 = int(range[1]) + else: + port = int(service) + except (TypeError, ValueError) as ex: + port = self.fw_model.get_service_port(service) + + if port is not None: + if port2 is not None and port2 <= port: + QMessageBox.warning(None, "Invalid service ports range", "Port {0} is lower than port {1}.".format(port2, port)) + else: + item = QubesFirewallRuleItem(address, netmask, port, port2) + if row is not None: + self.fw_model.setChild(row, item) + else: + self.fw_model.appendChild(item) + else: + QMessageBox.warning(None, "Invalid service name", "Service '{0} is unknown.".format(service)) + + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes VM Settings application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes VM Settings") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + vm = None + + if len(sys.argv) > 1: + vm = qvm_collection.get_vm_by_name(sys.argv[1]) + if vm is None or vm.qid not in qvm_collection: + QMessageBox.critical(None, "Qubes VM Settings Error", + "A VM with the name '{0}' does not exist in the system.".format(sys.argv[1])) + sys.exit(1) + else: + vms_list = [vm.name for vm in qvm_collection.values() if (vm.is_appvm() or vm.is_template())] + vmname = QInputDialog.getItem(None, "Select VM", "Select VM:", vms_list, editable = False) + if not vmname[1]: + sys.exit(1) + vm = qvm_collection.get_vm_by_name(vmname[0]) + + + global settings_window + settings_window = VMSettingsWindow(vm, app, qvm_collection, "basic") + + settings_window.show() + + app.exec_() + app.exit() + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/thread_monitor.py b/qubesmanager/thread_monitor.py new file mode 100644 index 0000000..4d5e915 --- /dev/null +++ b/qubesmanager/thread_monitor.py @@ -0,0 +1,44 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2011 Marek Marczykowski +# +# 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. +# +# + + +from PyQt4.QtCore import * + +import threading + +class ThreadMonitor(QObject): + def __init__(self): + self.success = True + self.error_msg = None + self.event_finished = threading.Event() + + def set_error_msg(self, error_msg): + self.success = False + self.error_msg = error_msg + self.set_finished() + + def is_finished(self): + return self.event_finished.is_set() + + def set_finished(self): + self.event_finished.set() + diff --git a/resources.qrc b/resources.qrc index c6e6649..935b43a 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,22 +1,35 @@ - - -icons/qubes.png -icons/appvm.png -icons/netvm.png -icons/networking.png -icons/dom0.png -icons/storagevm.png -icons/templatevm.png -icons/updateable.png -icons/home.png -icons/root.png -icons/createvm.png -icons/removevm.png -icons/shutdownvm.png -icons/resumevm.png -icons/pausevm.png -icons/showallvms.png -icons/showcpuload.png -icons/firewall.png - + + + icons/mount.png + icons/pencil.png + icons/redfirewall.png + icons/edit.png + icons/add.png + icons/flag-blue.png + icons/flag-green.png + icons/flag-red.png + icons/flag-yellow.png + icons/remove.png + icons/on.png + icons/appsprefs.png + icons/newfirewall.png + icons/qubes.png + icons/appvm.png + icons/netvm.png + icons/networking.png + icons/dom0.png + icons/storagevm.png + icons/templatevm.png + icons/updateable.png + icons/home.png + icons/root.png + icons/createvm.png + icons/removevm.png + icons/shutdownvm.png + icons/resumevm.png + icons/pausevm.png + icons/showallvms.png + icons/showcpuload.png + icons/firewall.png + diff --git a/restoredlg.ui b/restoredlg.ui new file mode 100644 index 0000000..3c908e9 --- /dev/null +++ b/restoredlg.ui @@ -0,0 +1,229 @@ + + + Restore + + + + 0 + 0 + 700 + 399 + + + + Qubes Restore VMs + + + + + + QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage + + + + + + + + 50 + false + + + + Backup source location + + + + + + + 50 + false + + + + Device + + + + + + + + 0 + 0 + + + + + dev1 + + + + + longdeviceblablabla + + + + + dev2 + + + + + dev3 + + + + + + + + Backup directory: + + + + + + + + + + ... + + + + + + + + + + + 50 + false + + + + Restore options + + + + + + Ignore missing templates or netvms, restore VMs anyway. + + + ignore missing + + + + + + + skip dom0 + + + + + + + Ignore dom0 username mismatch while restoring homedir. + + + ignore username mismatch + + + + + + + + + + Qt::Vertical + + + + 20 + 215 + + + + + + + + + + + + VMs to restore + + + + + + + + + + + + + 9 + 50 + false + false + false + + + + You're about to perform the following actions: + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> + + + + + + + + 9 + 50 + false + false + false + + + + To continue press Next. + + + + + + + + + + + + + + + + diff --git a/rpm_spec/qmgr.spec b/rpm_spec/qmgr.spec index 012c2c8..e45f853 100644 --- a/rpm_spec/qmgr.spec +++ b/rpm_spec/qmgr.spec @@ -33,12 +33,25 @@ cp qubes-appmenu-select $RPM_BUILD_ROOT/usr/bin mkdir -p $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager/ cp qubesmanager/main.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/appmenu_select.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/backup.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/backup_utils.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/firewall.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager -cp qubesmanager/qrc_resources.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/global_settings.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/multiselectwidget.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/restore.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/settings.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/thread_monitor.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/resources_rc.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/__init__.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_backupdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_editfwrulesdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_globalsettingsdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_mainwindow.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_multiselectwidget.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/ui_newappvmdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/ui_newfwruledlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager -cp qubesmanager/ui_editfwrulesdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_restoredlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_settingsdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager mkdir -p $RPM_BUILD_ROOT/usr/share/applications cp qubes-manager.desktop $RPM_BUILD_ROOT/usr/share/applications @@ -67,21 +80,60 @@ rm -rf $RPM_BUILD_ROOT %{python_sitearch}/qubesmanager/appmenu_select.py %{python_sitearch}/qubesmanager/appmenu_select.pyc %{python_sitearch}/qubesmanager/appmenu_select.pyo +%{python_sitearch}/qubesmanager/backup.py +%{python_sitearch}/qubesmanager/backup.pyc +%{python_sitearch}/qubesmanager/backup.pyo +%{python_sitearch}/qubesmanager/backup_utils.py +%{python_sitearch}/qubesmanager/backup_utils.pyc +%{python_sitearch}/qubesmanager/backup_utils.pyo %{python_sitearch}/qubesmanager/firewall.py %{python_sitearch}/qubesmanager/firewall.pyc %{python_sitearch}/qubesmanager/firewall.pyo -%{python_sitearch}/qubesmanager/qrc_resources.py -%{python_sitearch}/qubesmanager/qrc_resources.pyc -%{python_sitearch}/qubesmanager/qrc_resources.pyo +%{python_sitearch}/qubesmanager/global_settings.py +%{python_sitearch}/qubesmanager/global_settings.pyc +%{python_sitearch}/qubesmanager/global_settings.pyo +%{python_sitearch}/qubesmanager/multiselectwidget.py +%{python_sitearch}/qubesmanager/multiselectwidget.pyc +%{python_sitearch}/qubesmanager/multiselectwidget.pyo +%{python_sitearch}/qubesmanager/restore.py +%{python_sitearch}/qubesmanager/restore.pyc +%{python_sitearch}/qubesmanager/restore.pyo +%{python_sitearch}/qubesmanager/settings.py +%{python_sitearch}/qubesmanager/settings.pyc +%{python_sitearch}/qubesmanager/settings.pyo +%{python_sitearch}/qubesmanager/thread_monitor.py +%{python_sitearch}/qubesmanager/thread_monitor.pyc +%{python_sitearch}/qubesmanager/thread_monitor.pyo +%{python_sitearch}/qubesmanager/resources_rc.py +%{python_sitearch}/qubesmanager/resources_rc.pyc +%{python_sitearch}/qubesmanager/resources_rc.pyo +%{python_sitearch}/qubesmanager/ui_backupdlg.py +%{python_sitearch}/qubesmanager/ui_backupdlg.pyc +%{python_sitearch}/qubesmanager/ui_backupdlg.pyo +%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.py +%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyc +%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyo +%{python_sitearch}/qubesmanager/ui_globalsettingsdlg.py +%{python_sitearch}/qubesmanager/ui_globalsettingsdlg.pyc +%{python_sitearch}/qubesmanager/ui_globalsettingsdlg.pyo +%{python_sitearch}/qubesmanager/ui_mainwindow.py +%{python_sitearch}/qubesmanager/ui_mainwindow.pyc +%{python_sitearch}/qubesmanager/ui_mainwindow.pyo +%{python_sitearch}/qubesmanager/ui_multiselectwidget.py +%{python_sitearch}/qubesmanager/ui_multiselectwidget.pyc +%{python_sitearch}/qubesmanager/ui_multiselectwidget.pyo %{python_sitearch}/qubesmanager/ui_newappvmdlg.py %{python_sitearch}/qubesmanager/ui_newappvmdlg.pyc %{python_sitearch}/qubesmanager/ui_newappvmdlg.pyo %{python_sitearch}/qubesmanager/ui_newfwruledlg.py %{python_sitearch}/qubesmanager/ui_newfwruledlg.pyc %{python_sitearch}/qubesmanager/ui_newfwruledlg.pyo -%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.py -%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyc -%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyo +%{python_sitearch}/qubesmanager/ui_restoredlg.py +%{python_sitearch}/qubesmanager/ui_restoredlg.pyc +%{python_sitearch}/qubesmanager/ui_restoredlg.pyo +%{python_sitearch}/qubesmanager/ui_settingsdlg.py +%{python_sitearch}/qubesmanager/ui_settingsdlg.pyc +%{python_sitearch}/qubesmanager/ui_settingsdlg.pyo /usr/share/applications/qubes-manager.desktop diff --git a/settingsdlg.ui b/settingsdlg.ui new file mode 100644 index 0000000..99ece3d --- /dev/null +++ b/settingsdlg.ui @@ -0,0 +1,807 @@ + + + SettingsDialog + + + + 0 + 0 + 694 + 483 + + + + Settings + + + + :/root.png:/root.png + + + + + + + + true + + + + + + 5 + + + + + + + Basic + + + + + + Settings + + + + + + Name & label: + + + + + + + myappvm + + + + + + + true + + + + + + + Template: + + + + + + + + + + NetVM: + + + + + + + + + + false + + + Include in backups by default + + + true + + + + + + + + + + false + + + Info + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Type: + + + + + + + + 75 + true + + + + AppVM + + + + + + + Installed by RPM: + + + + + + + + 75 + true + + + + No + + + + + + + + + + false + + + Disk storage + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 10000 + + + 2 + + + + + + + GB + + + + + + + Private storage max. size: + + + + + + + + + + Qt::Vertical + + + + 20 + 73 + + + + + + + + + false + + + Advanced + + + + + + Memory/CPU + + + + + + Initial memory: + + + + + + + + 75 + true + + + + xx + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + MB + + + + + + + Max memory: + + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 10000 + + + 100 + + + 400 + + + + + + + MB + + + + + + + VCPUs no.: + + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + + + + + Include in memory balancing + + + + + + + + + + Kernel + + + + + + Kernel: + + + + + + + + + + Kernel opts: + + + + + + + + 75 + true + + + + [] + + + + + + + + + + Paths + + + + + + dir: + + + + + + + + 50 + false + + + + dir_path + + + + + + + config: + + + + + + + + 50 + false + + + + config_path + + + + + + + root img: + + + + + + + root_img_path + + + + + + + root volatile img: + + + + + + + volatile_path + + + + + + + private img: + + + + + + + private_path + + + + + + + + + + Qt::Vertical + + + + 20 + 88 + + + + + + + + + Firewall rules + + + + + + Allow network access except... + + + + + + + Deny network access except... + + + + + + + QLayout::SetMaximumSize + + + + + + + false + + + false + + + false + + + true + + + true + + + 40 + + + false + + + + + + + + + Allow ICMP traffic + + + true + + + + + + + Allow DNS queries + + + true + + + + + + + + + + + + + + + + + :/add.png:/add.png + + + + 24 + 24 + + + + + + + + + + + + :/pencil.png:/pencil.png + + + + 24 + 24 + + + + + + + + + + + + :/remove.png:/remove.png + + + + 24 + 24 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + false + + + Devices + + + + + + + + + + + :/appsprefs.png:/appsprefs.png + + + Applications + + + + + + + + + + false + + + Services + + + + + + + + + + + + + :/add.png:/add.png + + + + 24 + 24 + + + + + + + + + ntpd + + + Checked + + + + + cupsd + + + Checked + + + + + meminfo + + + Checked + + + + + + + + Checked services will be turned on. + + + + + + + Unchecked services will be turned off. + + + + + + + Unlisted services will follow default VM's settings. + + + + + + + + + + + :/remove.png:/remove.png + + + + 24 + 24 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/version b/version index 0664a8f..26aaba0 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.1.6 +1.2.0