Browse Source

Initial public commit.

(c) 2010 Invisible Things Lab

Author:
=========
Joanna Rutkowska <joanna@invisiblethingslab.com>
Joanna Rutkowska 14 years ago
commit
c1b81d3ab6

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+*.pyc

+ 13 - 0
Makefile

@@ -0,0 +1,13 @@
+RPMS_DIR=rpm/
+help:
+	@echo "make rpms -- generate binary rpm packages"
+	@echo "make res  -- compile resources"
+
+rpms:	
+	rpmbuild --define "_rpmdir $(RPMS_DIR)" -bb qmgr.spec
+	rpm --addsign $(RPMS_DIR)/x86_64/*.rpm
+
+
+res:
+	pyrcc4 -o qubesmanager/qrc_resources.py resources.qrc
+	pyuic4 -o qubesmanager/ui_newappvmdlg.py newappvmdlg.ui

BIN
icons/appvm.png


BIN
icons/createvm.png


BIN
icons/dom0.png


BIN
icons/home.png


BIN
icons/netvm.png


BIN
icons/networking.png


BIN
icons/pausevm.png


BIN
icons/qubes.png


+ 12 - 0
icons/readme.txt

@@ -0,0 +1,12 @@
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+This copyright and license notice covers the images in this directory.
+************************************************************************
+
+TITLE:	Crystal Project Icons
+AUTHOR:	Everaldo Coelho
+SITE:	http://www.everaldo.com
+CONTACT: everaldo@everaldo.com
+
+Copyright (c)  2006-2007  Everaldo Coelho.
+
+Additionally there are a few images taken from kde4 default icon theme.

BIN
icons/removevm.png


BIN
icons/resumevm.png


BIN
icons/root.png


BIN
icons/showallvms.png


BIN
icons/showcpuload.png


BIN
icons/shutdownvm.png


BIN
icons/storagevm.png


BIN
icons/templatevm.png


BIN
icons/updateable.png


+ 276 - 0
newappvmdlg.ui

@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewAppVMDlg</class>
+ <widget class="QDialog" name="NewAppVMDlg">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>507</width>
+    <height>209</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Create New AppVM</string>
+  </property>
+  <property name="windowIcon">
+   <iconset>
+    <normaloff>:/qubes.png</normaloff>:/qubes.png</iconset>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_3">
+   <item row="2" column="0">
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>Basic</string>
+      </attribute>
+      <layout class="QGridLayout" name="gridLayout_2">
+       <item row="0" column="1" colspan="2">
+        <widget class="QLineEdit" name="vmname">
+         <property name="text">
+          <string>myappvm</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="3">
+        <widget class="QComboBox" name="vmlabel">
+         <property name="frame">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QLabel" name="label">
+         <property name="text">
+          <string>Name &amp; label:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1" colspan="2">
+        <widget class="QComboBox" name="template_name"/>
+       </item>
+       <item row="1" column="0">
+        <widget class="QLabel" name="label_2">
+         <property name="text">
+          <string>Use this template:</string>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="0">
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="2" column="0" colspan="2">
+        <widget class="QCheckBox" name="checkBox_2">
+         <property name="enabled">
+          <bool>false</bool>
+         </property>
+         <property name="text">
+          <string>Allow networking</string>
+         </property>
+         <property name="checked">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_2">
+      <attribute name="title">
+       <string>Advanced</string>
+      </attribute>
+      <layout class="QGridLayout" name="gridLayout_4">
+       <item row="0" column="0">
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Disk storage</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout">
+          <item row="0" column="1">
+           <widget class="QSpinBox" name="priv_size">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+            </property>
+            <property name="maximum">
+             <number>10000</number>
+            </property>
+            <property name="value">
+             <number>2</number>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="0">
+           <widget class="QCheckBox" name="priv_allow_to_grow">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="text">
+             <string>Allow to grow</string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="2">
+           <widget class="QLabel" name="label_5">
+            <property name="text">
+             <string>GB</string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Private storage max. size</string>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="0">
+           <widget class="QCheckBox" name="checkBox">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="text">
+             <string>Include in backups</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>Memory/CPU</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout_5">
+          <item row="0" column="1">
+           <widget class="QSpinBox" name="mem_size">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+            </property>
+            <property name="maximum">
+             <number>10000</number>
+            </property>
+            <property name="singleStep">
+             <number>100</number>
+            </property>
+            <property name="value">
+             <number>400</number>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="2">
+           <widget class="QLabel" name="label_6">
+            <property name="text">
+             <string>MB</string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="1">
+           <widget class="QSpinBox" name="spinBox">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+            </property>
+            <property name="value">
+             <number>1</number>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="2">
+           <widget class="QLabel" name="label_4">
+            <property name="text">
+             <string>VCPUs</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item row="1" column="0">
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>NewAppVMDlg</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>NewAppVMDlg</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

+ 65 - 0
qmgr.spec

@@ -0,0 +1,65 @@
+%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
+
+%{!?version: %define version %(cat version)}
+
+Name:		qubes-manager
+Version:	%{version}
+Release:	1
+Summary:	The Graphical Qubes VM Manager.
+
+Group:		Qubes
+Vendor:		Invisible Things Lab
+License:	GPL
+URL:		http://fixme
+Requires:	python, PyQt4, qubes-core-dom0 >= 1.0.3, kdebase
+AutoReq:    0
+
+%define _builddir %(pwd)
+
+%description
+The Graphical Qubes VM Manager.
+
+%install
+
+mkdir -p $RPM_BUILD_ROOT/usr/bin/
+cp qubes-manager $RPM_BUILD_ROOT/usr/bin
+
+mkdir -p $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager/
+cp qubesmanager/main.py $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager
+cp qubesmanager/qrc_resources.py $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager
+cp qubesmanager/__init__.py $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager
+cp qubesmanager/ui_newappvmdlg.py $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager
+
+mkdir -p $RPM_BUILD_ROOT/usr/share/applications
+cp qubes-manager.desktop $RPM_BUILD_ROOT/usr/share/applications
+mkdir -p $RPM_BUILD_ROOT/etc/xdg/autostart/
+cp qubes-manager.desktop $RPM_BUILD_ROOT/etc/xdg/autostart/
+
+%post
+update-desktop-database &> /dev/null || :
+
+%postun
+update-desktop-database &> /dev/null || :
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%files
+%defattr(-,root,root,-)
+/usr/bin/qubes-manager
+%{python_sitearch}/qubesmanager/__init__.py
+%{python_sitearch}/qubesmanager/__init__.pyo
+%{python_sitearch}/qubesmanager/__init__.pyc
+%{python_sitearch}/qubesmanager/main.py
+%{python_sitearch}/qubesmanager/main.pyc
+%{python_sitearch}/qubesmanager/main.pyo
+%{python_sitearch}/qubesmanager/qrc_resources.py
+%{python_sitearch}/qubesmanager/qrc_resources.pyc
+%{python_sitearch}/qubesmanager/qrc_resources.pyo
+%{python_sitearch}/qubesmanager/ui_newappvmdlg.py
+%{python_sitearch}/qubesmanager/ui_newappvmdlg.pyc
+%{python_sitearch}/qubesmanager/ui_newappvmdlg.pyo
+
+
+/usr/share/applications/qubes-manager.desktop
+/etc/xdg/autostart/qubes-manager.desktop

+ 5 - 0
qubes-manager

@@ -0,0 +1,5 @@
+#!/usr/bin/python2.6
+import qubesmanager.main
+
+qubesmanager.main.main()
+

+ 10 - 0
qubes-manager.desktop

@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+Exec=qubes-manager
+Path=/var/lib/qubes
+Icon=/usr/share/qubes/icons/qubes.png
+Terminal=false
+Name=Qubes VM Manager
+GenericName=Qubes VM Manager
+StartupNotify=false
+Categories=System;

+ 2 - 0
qubesmanager/.gitignore

@@ -0,0 +1,2 @@
+qrc_resources.py
+ui_newappvmdlg.py

+ 0 - 0
qubesmanager/__init__.py


+ 759 - 0
qubesmanager/main.py

@@ -0,0 +1,759 @@
+#!/usr/bin/python2.6
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2010  Joanna Rutkowska <joanna@invisiblethingslab.com>
+#
+# 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
+from PyQt4.QtCore import *
+from PyQt4.QtGui import *
+
+from qubes.qubes import QubesVmCollection
+from qubes.qubes import QubesException
+from qubes.qubes import qubes_store_filename
+from qubes.qubes import QubesVmLabels
+from qubes.qubes import dry_run
+from qubes.qubes import qubes_guid_path
+from qubes.qubes import QubesDaemonPidfile
+
+import qubesmanager.qrc_resources
+import ui_newappvmdlg
+
+from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
+
+import subprocess
+import time
+import threading
+
+class QubesConfigFileWatcher(ProcessEvent):
+    def __init__ (self, update_func):
+        self.update_func = update_func
+        pass
+    
+    def process_IN_CLOSE_WRITE (self, event):
+        self.update_func()
+
+class VmStatusIcon(QLabel):
+    def __init__(self, vm, parent=None):
+        super (VmStatusIcon, self).__init__(parent)
+        self.vm = vm
+        (icon_pixmap, icon_sz) = self.set_vm_icon(self.vm)
+        self.setPixmap (icon_pixmap)
+        self.setFixedSize (icon_sz)
+        self.previous_power_state = vm.is_running()
+
+    def update(self):
+        if self.previous_power_state != self.vm.is_running():
+            (icon_pixmap, icon_sz) = self.set_vm_icon(self.vm)
+            self.setPixmap (icon_pixmap)
+            self.setFixedSize (icon_sz)
+            self.previous_power_state = self.vm.is_running()
+
+    def set_vm_icon(self, vm):
+        if vm.qid == 0:
+            icon = QIcon (":/dom0.png")
+        elif vm.is_appvm():
+            icon = QIcon (vm.label.icon_path)
+        elif vm.is_templete():
+            icon = QIcon (":/templatevm.png")
+        elif vm.is_netvm():
+            icon = QIcon (":/netvm.png")
+        else:
+            icon = QIcon()
+
+        icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.8)
+        if vm.is_running():
+            icon_pixmap = icon.pixmap(icon_sz)
+        else:
+            icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled)
+
+        return (icon_pixmap, icon_sz)
+
+
+class VmInfoWidget (QWidget):
+
+    def __init__(self, vm, parent = None):
+        super (VmInfoWidget, self).__init__(parent)
+
+        layout0 = QHBoxLayout()
+
+        label_name = QLabel (vm.name)
+
+        self.vm_running = vm.is_running()
+        layout0.addWidget(label_name, alignment=Qt.AlignLeft)
+
+        layout1 = QHBoxLayout()
+
+        if vm.is_appvm():
+            label_tmpl = QLabel ("<i><font color=\"gray\">" + vm.template_vm.name + "</i></font>")
+        elif vm.is_templete():
+            label_tmpl = QLabel ("<i><font color=\"gray\">TemplateVM</i></font>")
+        elif vm.qid == 0:
+            label_tmpl = QLabel ("<i><font color=\"gray\">AdminVM</i></font>")
+        elif vm.is_netvm():
+            label_tmpl = QLabel ("<i><font color=\"gray\">NetVM</i></font>")
+        else:
+            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(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.setLayout(layout3)
+
+    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_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
+
+    def update_vm_state (self, vm):
+        self.vm_icon.update()
+
+class LoadChartWidget (QWidget):
+
+    def __init__(self, vm, parent = None):
+        super (LoadChartWidget, self).__init__(parent)
+        self.load = vm.get_cpu_total_load() if vm.is_running() 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):
+        self.load = vm.get_cpu_total_load() if vm.is_running() 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:
+            N = W/dx
+            self.load_history.pop(0)
+
+        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 VmRowInTable(object):
+    def __init__(self, vm, row_no, table):
+        self.vm = vm
+        self.row_no = row_no
+
+        table.setRowHeight (row_no, VmManagerWindow.row_height)
+
+        self.info_widget = VmInfoWidget(vm)
+        table.setCellWidget(row_no, 0, self.info_widget)
+
+        self.load_widget = LoadChartWidget(vm)
+        table.setCellWidget(row_no, 1, self.load_widget)
+
+    def update(self, counter):
+        self.info_widget.update_vm_state(self.vm)
+        if counter % 3 == 0:
+            self.load_widget.update_load(self.vm)
+
+class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg):
+    def __init__(self, parent = None):
+        super (NewAppVmDlg, self).__init__(parent)
+        self.setupUi(self)
+
+vm_shutdown_timeout = 15000 # in msec
+
+class VmShutdownMonitor(QObject):
+    def __init__(self, vm):
+        self.vm = vm
+
+    def check_if_vm_has_shutdown(self):
+        vm = self.vm
+        if not vm.is_running():
+            return
+
+        reply = QMessageBox.question(None, "VM Shutdown", 
+                                     "The VM <b>'{0}'</b> hasn't shutdown within the last {1} seconds, do you want to kill it?<br>".format(vm_name, vm_shutdown_timeout/1000),
+                                     "Kill it!", "Wait another {0} seconds...".format(vm_shutdown_timeout/1000))
+
+        if reply == 0:
+            vm.force_shutdown()
+        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 = [200, 150]
+    row_height = 50
+    max_visible_rows = 14
+    update_interval = 1000 # in msec
+    show_inactive_vms = True
+
+    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/Resusme 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_updatevm = self.createAction ("Update VM", slot=None,
+                                             icon="updateable", tip="Update VM (only for 'updateable' VMs, e.g. templates)")
+
+        self.action_showallvms = self.createAction ("Show/Hide Inactive VMs", slot=None, 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_removevm.setDisabled(True)
+        self.action_resumevm.setDisabled(True)
+        self.action_pausevm.setDisabled(True)
+        self.action_shutdownvm.setDisabled(True)
+        self.action_updatevm.setDisabled(True)
+
+        self.action_showcpuload.setDisabled(True)
+
+        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_pausevm, self.action_shutdownvm, self.action_updatevm,
+                                   None,
+                                   self.action_showcpuload,
+                                   ))
+        
+        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().setStretchLastSection(True)
+        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.qvm_collection = QubesVmCollection()
+        self.setWindowTitle("Qubes VM Manager")
+ 
+        self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed)
+
+        self.fill_table()
+
+        tbl_W = 0
+        for (i, w) in enumerate(VmManagerWindow.columns_widths):
+            tbl_W += w
+
+        # TODO: '6' -- WTF?!
+        tbl_H = self.toolbar.height() + 6 + \
+                self.table.horizontalHeader().height() + 6
+
+        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.setMinimumWidth(tbl_W)
+        self.setGeometry(self.x(), self.y(), self.x() + tbl_W, self.y() + tbl_H)
+
+        self.counter = 0
+        self.shutdown_monitor = {}
+        QTimer.singleShot (self.update_interval, self.update_table)
+
+    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 get_vms_list(self):
+        self.qvm_collection.lock_db_for_reading()
+        self.qvm_collection.load()
+        self.qvm_collection.unlock_db()
+
+        if self.show_inactive_vms:
+            vms_list = [vm for vm in self.qvm_collection.values()]
+        else:
+            vms_list = [vm for vm in self.qvm_collection.values() if vm.is_running()]
+
+        no_vms = len (vms_list)
+        vms_to_display = []
+
+        # First, the NetVMs...
+        for netvm in vms_list:
+            if netvm.is_netvm():
+                vms_to_display.append (netvm)
+
+        # Now, the templates...
+        for tvm in vms_list:
+            if tvm.is_templete():
+                vms_to_display.append (tvm)
+
+        label_list = QubesVmLabels.values()
+        label_list.sort(key=lambda l: l.index)
+        for label in [label.name for label in label_list]:
+            for appvm in [vm for vm in vms_list if (vm.is_appvm() and vm.label.name == label)]:
+                vms_to_display.append(appvm)
+
+        assert len(vms_to_display) == no_vms
+        return vms_to_display
+
+    def fill_table(self):
+        self.table.clear()
+        vms_list = self.get_vms_list()
+        self.table.setRowCount(len(vms_list))
+
+        vms_in_table = []
+
+        for (row_no, vm) in enumerate(vms_list):
+            vm_row = VmRowInTable (vm, row_no, self.table)
+            vms_in_table.append (vm_row)
+
+        self.vms_list = vms_list
+        self.vms_in_table = vms_in_table
+        self.reload_table = False
+
+
+    def mark_table_for_update(self):
+        self.reload_table = True
+
+    # When calling update_table() directly, always use out_of_schedule=True!
+    def update_table(self, out_of_schedule=False):
+        if self.reload_table:
+            self.fill_table()
+
+        for vm_row in self.vms_in_table:
+            vm_row.update(self.counter)
+
+        self.table_selection_changed()
+
+        if not out_of_schedule:
+            self.counter += 1
+            QTimer.singleShot (self.update_interval, self.update_table)
+
+
+    def table_selection_changed (self):
+        vm = self.get_selected_vm()
+
+        # Update available actions:
+
+        self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.is_running())
+        #self.action_resumevm.setEnabled(not vm.is_running())
+        #self.action_pausevm.setEnabled(vm.is_running() and vm.qid != 0)
+        self.action_shutdownvm.setEnabled(vm.is_running() and vm.qid != 0)
+
+    def closeEvent (self, event):
+        self.hide()
+        event.ignore()
+
+    def create_appvm(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
+
+        self.qvm_collection.lock_db_for_reading()
+        self.qvm_collection.load()
+        self.qvm_collection.unlock_db()
+
+        label_list = QubesVmLabels.values()
+        label_list.sort(key=lambda l: l.index)
+        for (i, label) in enumerate(label_list):
+            dialog.vmlabel.insertItem(i, label.name)
+            dialog.vmlabel.setItemIcon (i, QIcon(label.icon_path))
+
+        template_vm_list = [vm for vm in self.qvm_collection.values() if vm.is_templete()]
+
+        default_index = 0
+        for (i, vm) in enumerate(template_vm_list):
+            if vm is self.qvm_collection.get_default_template_vm():
+                default_index = i
+                dialog.template_name.insertItem(i, vm.name + " (default)")
+            else:
+                dialog.template_name.insertItem(i, vm.name)
+        dialog.template_name.setCurrentIndex(default_index)
+
+        dialog.vmname.selectAll()
+        dialog.vmname.setFocus()
+
+        if dialog.exec_():
+            vmname = str(dialog.vmname.text())
+            if self.qvm_collection.get_vm_by_name(vmname) is not None:
+                QMessageBox.warning (None, "Incorrect AppVM Name!", "A VM with the name <b>{0}</b> already exists in the system!".format(vmname))
+                return
+
+            label = label_list[dialog.vmlabel.currentIndex()]
+            template_vm = template_vm_list[dialog.template_name.currentIndex()]
+
+            thread_monitor = ThreadMonitor()
+            thread = threading.Thread (target=self.do_create_appvm, args=(vmname, label, template_vm, thread_monitor))
+            thread.daemon = True
+            thread.start()
+
+            progress = QProgressDialog ("Creating new AppVM <b>{0}</b>...".format(vmname), "", 0, 0)
+            progress.setCancelButton(None)
+            progress.setModal(True)
+            progress.show()
+            
+            while not thread_monitor.is_finished():
+                app.processEvents()
+                time.sleep (0.1)
+
+            progress.hide()
+
+            if thread_monitor.success:
+                trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been created.".format(vmname), msecs=3000)
+            else:
+                QMessageBox.warning (None, "Error creating AppVM!", "ERROR: {0}".format(thread_monitor.error_msg))
+
+
+    def do_create_appvm (self, vmname, label, template_vm, thread_monitor):
+        try:
+            self.qvm_collection.lock_db_for_writing()
+            self.qvm_collection.load()
+
+            vm = self.qvm_collection.add_new_appvm(vmname, template_vm, label = label)
+            vm.create_on_disk(verbose=False)
+            vm.add_to_xen_storage()
+            self.qvm_collection.save()
+        except Exception as ex:
+            thread_monitor.set_error_msg (str(ex))
+            vm.remove_from_disk()
+        finally:
+            self.qvm_collection.unlock_db()
+
+        thread_monitor.set_finished()
+
+
+    def get_selected_vm(self):
+        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
+
+    def remove_appvm(self):
+        vm = self.get_selected_vm()
+        assert not vm.is_running()
+        assert not vm.installed_by_rpm
+
+        self.qvm_collection.lock_db_for_reading()
+        self.qvm_collection.load()
+        self.qvm_collection.unlock_db()
+ 
+        if vm.is_templete():
+            dependent_vms = self.qvm_collection.get_vms_based_on(vm.qid)
+            if len(dependent_vms) > 0:
+                QMessageBox.warning (None, "Warning!", 
+                                     "This Template VM cannot be removed, because there is at least one AppVM that is based on it.<br>"
+                                     "<small>If you want to remove this Template VM and all the AppVMs based on it,"
+                                     "you should first remove each individual AppVM that uses this template.</small>")
+
+                return
+
+        reply = QMessageBox.question(None, "VM Removal Confirmation", 
+                                     "Are you sure you want to remove the VM <b>'{0}'</b>?<br>"
+                                     "<small>All data on this VM's private storage will be lost!</small>".format(vm.name),
+                                     QMessageBox.Yes | QMessageBox.Cancel)
+
+
+        if reply == QMessageBox.Yes:
+
+            thread_monitor = ThreadMonitor()
+            thread = threading.Thread (target=self.do_remove_vm, args=(vm, thread_monitor))
+            thread.daemon = True
+            thread.start()
+
+            progress = QProgressDialog ("Removing VM: <b>{0}</b>...".format(vm.name), "", 0, 0)
+            progress.setCancelButton(None)
+            progress.setModal(True)
+            progress.show()
+            
+            while not thread_monitor.is_finished():
+                app.processEvents()
+                time.sleep (0.1)
+
+            progress.hide()
+
+            if thread_monitor.success:
+                trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been removed.".format(vm.name), msecs=3000)
+            else:
+                QMessageBox.warning (None, "Error removing M!", "ERROR: {0}".format(thread_monitor.error_msg))
+
+    def do_remove_vm (self, vm, thread_monitor):
+        try:
+            self.qvm_collection.lock_db_for_writing()
+            self.qvm_collection.load()
+
+            #TODO: the following two conditions should really be checked by qvm_collection.pop() overload...
+            if vm.is_templete() and qvm_collection.default_template_qid == vm.qid:
+                qvm_collection.default_template_qid = None
+            if vm.is_netvm() and qvm_collection.default_netvm_qid == vm.qid:
+                qvm_collection.default_netvm_qid = None
+
+            vm.remove_from_xen_storage()
+            vm.remove_from_disk()
+            self.qvm_collection.pop(vm.qid)
+            self.qvm_collection.save()
+        except Exception as ex:
+            thread_monitor.set_error_msg (str(ex))
+        finally:
+            self.qvm_collection.unlock_db()
+
+        thread_monitor.set_finished()
+
+    def resume_vm(self):
+        pass
+ 
+    def pause_vm(self):
+        pass
+
+    def shutdown_vm(self):
+        vm = self.get_selected_vm()
+        assert vm.is_running()
+
+        reply = QMessageBox.question(None, "VM Shutdown Confirmation", 
+                                     "Are you sure you want to power down the VM <b>'{0}'</b>?<br>"
+                                     "<small>This will shutdown all the running applications within this VM.</small>".format(vm.name),
+                                     QMessageBox.Yes | QMessageBox.Cancel)
+
+        if reply == QMessageBox.Yes:
+            try:
+                subprocess.check_call (["/usr/sbin/xm", "shutdown", vm.name])
+            except Exception as ex:
+                QMessageBox.warning (None, "Error shutting down VM!", "ERROR: {0}".format(ex))
+                return
+
+            trayIcon.showMessage ("Qubes Manager", "VM '{0}' is shutting down...".format(vm.name), msecs=3000)
+            self.shutdown_monitor[vm.qid] = VmShutdownMonitor (vm)
+            QTimer.singleShot (vm_shutdown_timeout, self.shutdown_monitor[vm.qid].check_if_vm_has_shutdown)
+
+    def showcpuload(self):
+        pass
+
+                   
+class QubesTrayIcon(QSystemTrayIcon):
+    def __init__(self, icon):
+        QSystemTrayIcon.__init__(self, icon)
+        self.menu = QMenu()
+
+        action_showmanager = self.createAction ("Open VM Manager", slot=show_manager, icon="qubes")
+        action_backup = self.createAction ("Make backup")
+        action_preferences = self.createAction ("Preferences")
+        action_set_netvm = self.createAction ("Set default NetVM", icon="networking")
+        action_sys_info = self.createAction ("System Info", icon="dom0")
+        action_exit = self.createAction ("Exit", slot=exit_app)
+
+        action_backup.setDisabled(True)
+        action_preferences.setDisabled(True)
+        action_set_netvm.setDisabled(True)
+        action_sys_info.setDisabled(True)
+
+        self.addActions (self.menu, (action_showmanager, action_backup, action_sys_info, None, action_preferences, action_set_netvm, None, action_exit))
+
+        self.setContextMenu(self.menu)
+
+        self.connect (self, SIGNAL("activated (QSystemTrayIcon::ActivationReason)"), self.icon_clicked)
+
+    def icon_clicked(self, reason):
+        if reason == QSystemTrayIcon.Context:
+            # Handle the right click normally, i.e. display the context menu
+            return
+        else:
+            show_manager()
+
+    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 show_manager():
+    manager_window.show()
+
+
+def exit_app():
+    notifier.stop()
+    app.exit()
+
+
+# Bases on the original code by:
+# Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
+
+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 Manager.<br><br>"
+                         "<b><i>%s</i></b>" % error +
+                         "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
+                         % ( line, filename ))
+
+    #sys.exit(1)
+
+def main():
+
+
+    # Avoid starting more than one instance of the app
+    lock = QubesDaemonPidfile ("qubes-manager")
+    if lock.pidfile_exists():
+        if lock.pidfile_is_stale():
+            lock.remove_pidfile()
+            print "Removed stale pidfile (has the previous daemon instance crashed?)."
+        else:
+            exit (0)
+
+    lock.create_pidfile()
+
+    global app
+    app = QApplication(sys.argv)
+    app.setOrganizationName("The Qubes Project")
+    app.setOrganizationDomain("http://qubes-os.org")
+    app.setApplicationName("Qubes VM Manager")
+    app.setWindowIcon(QIcon(":/qubes.png"))
+
+    sys.excepthook = handle_exception
+
+    global manager_window
+    manager_window = VmManagerWindow()
+    wm = WatchManager()
+    mask = EventsCodes.OP_FLAGS.get('IN_CLOSE_WRITE')
+
+    global notifier
+    notifier = ThreadedNotifier(wm, QubesConfigFileWatcher(manager_window.mark_table_for_update))
+    notifier.start()
+    wdd = wm.add_watch(qubes_store_filename, mask)
+
+    global trayIcon
+    trayIcon = QubesTrayIcon(QIcon(":/qubes.png"))
+    trayIcon.show()
+
+    app.exec_()
+    trayIcon = None
+

+ 21 - 0
resources.qrc

@@ -0,0 +1,21 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+<file alias="qubes.png">icons/qubes.png</file>
+<file alias="appvm.png">icons/appvm.png</file>
+<file alias="netvm.png">icons/netvm.png</file>
+<file alias="networking.png">icons/networking.png</file>
+<file alias="dom0.png">icons/dom0.png</file>
+<file alias="storagevm.png">icons/storagevm.png</file>
+<file alias="templatevm.png">icons/templatevm.png</file>
+<file alias="updateable.png">icons/updateable.png</file>
+<file alias="home.png">icons/home.png</file>
+<file alias="root.png">icons/root.png</file>
+<file alias="createvm.png">icons/createvm.png</file>
+<file alias="removevm.png">icons/removevm.png</file>
+<file alias="shutdownvm.png">icons/shutdownvm.png</file>
+<file alias="resumevm.png">icons/resumevm.png</file>
+<file alias="pausevm.png">icons/pausevm.png</file>
+<file alias="showallvms.png">icons/showallvms.png</file>
+<file alias="showcpuload.png">icons/showcpuload.png</file>
+</qresource>
+</RCC>

+ 1 - 0
version

@@ -0,0 +1 @@
+0.1.1