main.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. #!/usr/bin/python2.6
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License
  9. # as published by the Free Software Foundation; either version 2
  10. # of the License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. #
  21. #
  22. import sys
  23. from PyQt4.QtCore import *
  24. from PyQt4.QtGui import *
  25. from qubes.qubes import QubesVmCollection
  26. from qubes.qubes import QubesException
  27. from qubes.qubes import qubes_store_filename
  28. from qubes.qubes import QubesVmLabels
  29. from qubes.qubes import dry_run
  30. from qubes.qubes import qubes_guid_path
  31. from qubes.qubes import QubesDaemonPidfile
  32. from qubes.qubes import QubesHost
  33. import qubesmanager.qrc_resources
  34. import ui_newappvmdlg
  35. from appmenu_select import AppmenuSelectWindow
  36. from firewall import EditFwRulesDlg, QubesFirewallRulesModel
  37. from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
  38. import subprocess
  39. import time
  40. import threading
  41. from datetime import datetime,timedelta
  42. qubes_guid_path = '/usr/bin/qubes_guid'
  43. class QubesConfigFileWatcher(ProcessEvent):
  44. def __init__ (self, update_func):
  45. self.update_func = update_func
  46. def process_IN_MODIFY (self, event):
  47. self.update_func()
  48. class VmStatusIcon(QLabel):
  49. def __init__(self, vm, parent=None):
  50. super (VmStatusIcon, self).__init__(parent)
  51. self.vm = vm
  52. (icon_pixmap, icon_sz) = self.set_vm_icon(self.vm)
  53. self.setPixmap (icon_pixmap)
  54. self.setFixedSize (icon_sz)
  55. self.previous_power_state = vm.last_power_state
  56. def update(self):
  57. if self.previous_power_state != self.vm.last_power_state:
  58. (icon_pixmap, icon_sz) = self.set_vm_icon(self.vm)
  59. self.setPixmap (icon_pixmap)
  60. self.setFixedSize (icon_sz)
  61. self.previous_power_state = self.vm.last_power_state
  62. def set_vm_icon(self, vm):
  63. if vm.qid == 0:
  64. icon = QIcon (":/dom0.png")
  65. elif vm.is_appvm():
  66. icon = QIcon (vm.label.icon_path)
  67. elif vm.is_template():
  68. icon = QIcon (":/templatevm.png")
  69. elif vm.is_netvm():
  70. icon = QIcon (":/netvm.png")
  71. else:
  72. icon = QIcon()
  73. icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.8)
  74. if vm.last_power_state:
  75. icon_pixmap = icon.pixmap(icon_sz)
  76. else:
  77. icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled)
  78. return (icon_pixmap, icon_sz)
  79. class VmInfoWidget (QWidget):
  80. def __init__(self, vm, parent = None):
  81. super (VmInfoWidget, self).__init__(parent)
  82. layout0 = QHBoxLayout()
  83. self.label_name = QLabel (vm.name)
  84. self.vm_running = vm.last_power_state
  85. layout0.addWidget(self.label_name, alignment=Qt.AlignLeft)
  86. layout1 = QHBoxLayout()
  87. if vm.template_vm is not None:
  88. self.label_tmpl = QLabel ("<i><font color=\"gray\">" + (vm.template_vm.name) + "</i></font>")
  89. elif vm.is_appvm(): # and vm.template_vm is None
  90. self.label_tmpl = QLabel ("<i><font color=\"gray\">StandaloneVM</i></font>")
  91. elif vm.is_template():
  92. self.label_tmpl = QLabel ("<i><font color=\"gray\">TemplateVM</i></font>")
  93. elif vm.qid == 0:
  94. self.label_tmpl = QLabel ("<i><font color=\"gray\">AdminVM</i></font>")
  95. elif vm.is_netvm():
  96. self.label_tmpl = QLabel ("<i><font color=\"gray\">NetVM</i></font>")
  97. else:
  98. self.label_tmpl = QLabel ("")
  99. label_icon_networked = self.set_icon(":/networking.png", vm.is_networked())
  100. layout1.addWidget(label_icon_networked, alignment=Qt.AlignLeft)
  101. if vm.is_updateable():
  102. label_icon_updtbl = self.set_icon(":/updateable.png", True)
  103. layout1.addWidget(label_icon_updtbl, alignment=Qt.AlignLeft)
  104. layout1.addWidget(self.label_tmpl, alignment=Qt.AlignLeft)
  105. layout1.addStretch()
  106. layout2 = QVBoxLayout ()
  107. layout2.addLayout(layout0)
  108. layout2.addLayout(layout1)
  109. layout3 = QHBoxLayout ()
  110. self.vm_icon = VmStatusIcon(vm)
  111. layout3.addWidget(self.vm_icon)
  112. layout3.addSpacing (10)
  113. layout3.addLayout(layout2)
  114. self.setLayout(layout3)
  115. self.previous_outdated = False
  116. def set_icon(self, icon_path, enabled = True):
  117. label_icon = QLabel()
  118. icon = QIcon (icon_path)
  119. icon_sz = QSize (VmManagerWindow.row_height * 0.3, VmManagerWindow.row_height * 0.3)
  120. icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal)
  121. label_icon.setPixmap (icon_pixmap)
  122. label_icon.setFixedSize (icon_sz)
  123. return label_icon
  124. def update_vm_state (self, vm):
  125. self.vm_icon.update()
  126. def update_outdated(self, vm):
  127. outdated = vm.is_outdated()
  128. if outdated != self.previous_outdated:
  129. if outdated:
  130. self.label_name.setText(vm.name + "<small><font color=\"red\"> (outdated)</font></small>")
  131. else:
  132. self.label_name.setText(vm.name)
  133. self.previous_outdated = outdated
  134. class VmUsageWidget (QWidget):
  135. def __init__(self, vm, cpu_load = 0, parent = None):
  136. super (VmUsageWidget, self).__init__(parent)
  137. self.cpu_widget = QProgressBar()
  138. self.mem_widget = QProgressBar()
  139. self.cpu_widget.setMinimum(0)
  140. self.cpu_widget.setMaximum(100)
  141. self.mem_widget.setMinimum(0)
  142. self.mem_widget.setMaximum(qubes_host.memory_total/1024)
  143. self.mem_widget.setFormat ("%v MB");
  144. self.cpu_label = QLabel("CPU")
  145. self.mem_label = QLabel("MEM")
  146. layout_cpu = QHBoxLayout()
  147. layout_cpu.addWidget(self.cpu_label)
  148. layout_cpu.addWidget(self.cpu_widget)
  149. layout_mem = QHBoxLayout()
  150. layout_mem.addWidget(self.mem_label)
  151. layout_mem.addWidget(self.mem_widget)
  152. layout = QVBoxLayout()
  153. layout.addLayout(layout_cpu)
  154. layout.addLayout(layout_mem)
  155. self.setLayout(layout)
  156. self.update_load(vm, cpu_load)
  157. def update_load(self, vm, cpu_load):
  158. self.cpu_load = cpu_load if vm.last_power_state else 0
  159. self.mem_load = vm.get_mem()/1024 if vm.last_power_state else 0
  160. self.cpu_widget.setValue(self.cpu_load)
  161. self.mem_widget.setValue(self.mem_load)
  162. def resizeEvent(self, Event = None):
  163. label_width = max(self.mem_label.width(), self.cpu_label.width())
  164. self.mem_label.setMinimumWidth(label_width)
  165. self.cpu_label.setMinimumWidth(label_width)
  166. super (VmUsageWidget, self).resizeEvent(Event)
  167. class LoadChartWidget (QWidget):
  168. def __init__(self, vm, cpu_load = 0, parent = None):
  169. super (LoadChartWidget, self).__init__(parent)
  170. self.load = cpu_load if vm.last_power_state else 0
  171. assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  172. self.load_history = [self.load]
  173. def update_load (self, vm, cpu_load):
  174. self.load = cpu_load if vm.last_power_state else 0
  175. assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  176. self.load_history.append (self.load)
  177. self.repaint()
  178. def paintEvent (self, Event = None):
  179. p = QPainter (self)
  180. dx = 4
  181. W = self.width()
  182. H = self.height() - 5
  183. N = len(self.load_history)
  184. if N > W/dx:
  185. tail = N - W/dx
  186. N = W/dx
  187. self.load_history = self.load_history[tail:]
  188. assert len(self.load_history) == N
  189. for i in range (0, N-1):
  190. val = self.load_history[N- i - 1]
  191. hue = 200
  192. sat = 70 + val*(255-70)/100
  193. color = QColor.fromHsv (hue, sat, 255)
  194. pen = QPen (color)
  195. pen.setWidth(dx-1)
  196. p.setPen(pen)
  197. if val > 0:
  198. p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100)
  199. class MemChartWidget (QWidget):
  200. def __init__(self, vm, parent = None):
  201. super (MemChartWidget, self).__init__(parent)
  202. self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0
  203. assert self.load >= 0 and self.load <= 100, "mem = {0}".format(self.load)
  204. self.load_history = [self.load]
  205. def update_load (self, vm):
  206. self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0
  207. assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  208. self.load_history.append (self.load)
  209. self.repaint()
  210. def paintEvent (self, Event = None):
  211. p = QPainter (self)
  212. dx = 4
  213. W = self.width()
  214. H = self.height() - 5
  215. N = len(self.load_history)
  216. if N > W/dx:
  217. tail = N - W/dx
  218. N = W/dx
  219. self.load_history = self.load_history[tail:]
  220. assert len(self.load_history) == N
  221. for i in range (0, N-1):
  222. val = self.load_history[N- i - 1]
  223. hue = 120
  224. sat = 70 + val*(255-70)/100
  225. color = QColor.fromHsv (hue, sat, 255)
  226. pen = QPen (color)
  227. pen.setWidth(dx-1)
  228. p.setPen(pen)
  229. if val > 0:
  230. p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100)
  231. class VmRowInTable(object):
  232. def __init__(self, vm, row_no, table):
  233. self.vm = vm
  234. self.row_no = row_no
  235. table.setRowHeight (row_no, VmManagerWindow.row_height)
  236. self.info_widget = VmInfoWidget(vm)
  237. table.setCellWidget(row_no, 0, self.info_widget)
  238. self.usage_widget = VmUsageWidget(vm)
  239. table.setCellWidget(row_no, 1, self.usage_widget)
  240. self.load_widget = LoadChartWidget(vm)
  241. table.setCellWidget(row_no, 2, self.load_widget)
  242. self.mem_widget = MemChartWidget(vm)
  243. table.setCellWidget(row_no, 3, self.mem_widget)
  244. def update(self, counter, cpu_load = None):
  245. self.info_widget.update_vm_state(self.vm)
  246. if cpu_load is not None:
  247. self.usage_widget.update_load(self.vm, cpu_load)
  248. self.load_widget.update_load(self.vm, cpu_load)
  249. self.mem_widget.update_load(self.vm)
  250. self.info_widget.update_outdated(self.vm)
  251. class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg):
  252. def __init__(self, parent = None):
  253. super (NewAppVmDlg, self).__init__(parent)
  254. self.setupUi(self)
  255. vm_shutdown_timeout = 15000 # in msec
  256. class VmShutdownMonitor(QObject):
  257. def __init__(self, vm):
  258. self.vm = vm
  259. def check_if_vm_has_shutdown(self):
  260. vm = self.vm
  261. if not vm.is_running() or vm.get_start_time() >= datetime.utcnow() - timedelta(0,vm_shutdown_timeout/1000):
  262. if vm.is_template():
  263. trayIcon.showMessage ("Qubes Manager", "You have just modified template '{0}'. You should now restart all the VMs based on it, so they could see the changes.".format(vm.name), msecs=8000)
  264. return
  265. reply = QMessageBox.question(None, "VM Shutdown",
  266. "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),
  267. "Kill it!", "Wait another {0} seconds...".format(vm_shutdown_timeout/1000))
  268. if reply == 0:
  269. vm.force_shutdown()
  270. else:
  271. QTimer.singleShot (vm_shutdown_timeout, self.check_if_vm_has_shutdown)
  272. class ThreadMonitor(QObject):
  273. def __init__(self):
  274. self.success = True
  275. self.error_msg = None
  276. self.event_finished = threading.Event()
  277. def set_error_msg(self, error_msg):
  278. self.success = False
  279. self.error_msg = error_msg
  280. self.set_finished()
  281. def is_finished(self):
  282. return self.event_finished.is_set()
  283. def set_finished(self):
  284. self.event_finished.set()
  285. class VmManagerWindow(QMainWindow):
  286. columns_widths = [200, 200, 150, 150]
  287. row_height = 50
  288. max_visible_rows = 14
  289. update_interval = 1000 # in msec
  290. show_inactive_vms = True
  291. columns_states = { 0: [0, 1], 1: [0, 2, 3] }
  292. def __init__(self, parent=None):
  293. super(VmManagerWindow, self).__init__(parent)
  294. self.action_createvm = self.createAction ("Create AppVM", slot=self.create_appvm,
  295. icon="createvm", tip="Create a new AppVM")
  296. self.action_removevm = self.createAction ("Remove AppVM", slot=self.remove_appvm,
  297. icon="removevm", tip="Remove an existing AppVM (must be stopped first)")
  298. self.action_resumevm = self.createAction ("Start/Resume VM", slot=self.resume_vm,
  299. icon="resumevm", tip="Start/Resume a VM")
  300. self.action_pausevm = self.createAction ("Pause VM", slot=self.pause_vm,
  301. icon="pausevm", tip="Pause a running VM")
  302. self.action_shutdownvm = self.createAction ("Shutdown VM", slot=self.shutdown_vm,
  303. icon="shutdownvm", tip="Shutdown a running VM")
  304. self.action_appmenus = self.createAction ("Select VM applications", slot=self.appmenus_select,
  305. icon="root", tip="Select applications present in menu for this VM")
  306. self.action_showallvms = self.createAction ("Show/Hide Inactive VMs", slot=self.toggle_inactive_view, checkable=True,
  307. icon="showallvms", tip="Show/Hide Inactive VMs")
  308. self.action_showcpuload = self.createAction ("Show/Hide CPU Load chart", slot=self.showcpuload, checkable=True,
  309. icon="showcpuload", tip="Show/Hide CPU Load chart")
  310. self.action_editfwrules = self.createAction ("Edit VM Firewall rules", slot=self.edit_fw_rules,
  311. icon="firewall", tip="Edit VM Firewall rules")
  312. self.action_removevm.setDisabled(True)
  313. self.action_resumevm.setDisabled(True)
  314. self.action_pausevm.setDisabled(True)
  315. self.action_shutdownvm.setDisabled(True)
  316. self.action_appmenus.setDisabled(True)
  317. self.action_showallvms.setChecked(self.show_inactive_vms)
  318. self.toolbar = self.addToolBar ("Toolbar")
  319. self.toolbar.setFloatable(False)
  320. self.addActions (self.toolbar, (self.action_createvm, self.action_removevm,
  321. None,
  322. self.action_resumevm, self.action_shutdownvm,
  323. self.action_editfwrules, self.action_appmenus,
  324. None,
  325. self.action_showcpuload,
  326. self.action_showallvms,
  327. ))
  328. self.table = QTableWidget()
  329. self.setCentralWidget(self.table)
  330. self.table.clear()
  331. self.table.setColumnCount(len(VmManagerWindow.columns_widths))
  332. for (col, width) in enumerate (VmManagerWindow.columns_widths):
  333. self.table.setColumnWidth (col, width)
  334. self.table.horizontalHeader().setResizeMode(QHeaderView.Stretch)
  335. self.table.horizontalHeader().setResizeMode(0, QHeaderView.Fixed)
  336. self.table.setAlternatingRowColors(True)
  337. self.table.verticalHeader().hide()
  338. self.table.horizontalHeader().hide()
  339. self.table.setGridStyle(Qt.NoPen)
  340. self.table.setSortingEnabled(False)
  341. self.table.setSelectionBehavior(QTableWidget.SelectRows)
  342. self.table.setSelectionMode(QTableWidget.SingleSelection)
  343. self.__cpugraphs = self.action_showcpuload.isChecked()
  344. self.update_table_columns()
  345. self.qvm_collection = QubesVmCollection()
  346. self.setWindowTitle("Qubes VM Manager")
  347. self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed)
  348. cur_pos = self.pos()
  349. self.setFixedWidth (self.get_minimum_table_width())
  350. self.fill_table()
  351. self.move(cur_pos)
  352. self.counter = 0
  353. self.shutdown_monitor = {}
  354. self.last_measure_results = {}
  355. self.last_measure_time = time.time()
  356. QTimer.singleShot (self.update_interval, self.update_table)
  357. def set_table_geom_height(self):
  358. # TODO: '6' -- WTF?!
  359. tbl_H = self.toolbar.height() + 6 + \
  360. self.table.horizontalHeader().height() + 6
  361. n = self.table.rowCount();
  362. if n > VmManagerWindow.max_visible_rows:
  363. n = VmManagerWindow.max_visible_rows
  364. for i in range (0, n):
  365. tbl_H += self.table.rowHeight(i)
  366. self.setFixedHeight(tbl_H)
  367. def addActions(self, target, actions):
  368. for action in actions:
  369. if action is None:
  370. target.addSeparator()
  371. else:
  372. target.addAction(action)
  373. def createAction(self, text, slot=None, shortcut=None, icon=None,
  374. tip=None, checkable=False, signal="triggered()"):
  375. action = QAction(text, self)
  376. if icon is not None:
  377. action.setIcon(QIcon(":/%s.png" % icon))
  378. if shortcut is not None:
  379. action.setShortcut(shortcut)
  380. if tip is not None:
  381. action.setToolTip(tip)
  382. action.setStatusTip(tip)
  383. if slot is not None:
  384. self.connect(action, SIGNAL(signal), slot)
  385. if checkable:
  386. action.setCheckable(True)
  387. return action
  388. def get_vms_list(self):
  389. self.qvm_collection.lock_db_for_reading()
  390. self.qvm_collection.load()
  391. self.qvm_collection.unlock_db()
  392. vms_list = [vm for vm in self.qvm_collection.values()]
  393. for vm in vms_list:
  394. vm.last_power_state = vm.is_running()
  395. no_vms = len (vms_list)
  396. vms_to_display = []
  397. # First, the NetVMs...
  398. for netvm in vms_list:
  399. if netvm.is_netvm():
  400. vms_to_display.append (netvm)
  401. # Now, the templates...
  402. for tvm in vms_list:
  403. if tvm.is_template():
  404. vms_to_display.append (tvm)
  405. label_list = QubesVmLabels.values()
  406. label_list.sort(key=lambda l: l.index)
  407. for label in [label.name for label in label_list]:
  408. for appvm in [vm for vm in vms_list if ((vm.is_appvm() or vm.is_disposablevm()) and vm.label.name == label)]:
  409. vms_to_display.append(appvm)
  410. assert len(vms_to_display) == no_vms
  411. return vms_to_display
  412. def fill_table(self):
  413. self.table.clear()
  414. vms_list = self.get_vms_list()
  415. self.table.setRowCount(len(vms_list))
  416. vms_in_table = []
  417. row_no = 0
  418. for vm in vms_list:
  419. if (not self.show_inactive_vms) and (not vm.last_power_state):
  420. continue
  421. if vm.internal:
  422. continue
  423. vm_row = VmRowInTable (vm, row_no, self.table)
  424. vms_in_table.append (vm_row)
  425. row_no += 1
  426. self.table.setRowCount(row_no)
  427. self.set_table_geom_height()
  428. self.vms_list = vms_list
  429. self.vms_in_table = vms_in_table
  430. self.reload_table = False
  431. def mark_table_for_update(self):
  432. self.reload_table = True
  433. # When calling update_table() directly, always use out_of_schedule=True!
  434. def update_table(self, out_of_schedule=False):
  435. if manager_window.isVisible():
  436. some_vms_have_changed_power_state = False
  437. for vm in self.vms_list:
  438. state = vm.is_running();
  439. if vm.last_power_state != state:
  440. vm.last_power_state = state
  441. some_vms_have_changed_power_state = True
  442. if self.reload_table or ((not self.show_inactive_vms) and some_vms_have_changed_power_state):
  443. self.fill_table()
  444. if self.counter % 3 == 0 or out_of_schedule:
  445. (self.last_measure_time, self.last_measure_results) = \
  446. qubes_host.measure_cpu_usage(self.last_measure_results,
  447. self.last_measure_time)
  448. for vm_row in self.vms_in_table:
  449. cur_cpu_load = None
  450. if vm_row.vm.get_xid() in self.last_measure_results:
  451. cur_cpu_load = self.last_measure_results[vm_row.vm.xid]['cpu_usage']
  452. else:
  453. cur_cpu_load = 0
  454. vm_row.update(self.counter, cpu_load = cur_cpu_load)
  455. else:
  456. for vm_row in self.vms_in_table:
  457. vm_row.update(self.counter)
  458. self.table_selection_changed()
  459. if not out_of_schedule:
  460. self.counter += 1
  461. QTimer.singleShot (self.update_interval, self.update_table)
  462. def update_table_columns(self):
  463. state = 1 if self.__cpugraphs else 0
  464. columns = self.columns_states[state]
  465. for i in range(0, self.table.columnCount()):
  466. enabled = columns.count(i) > 0
  467. self.table.setColumnHidden(i, not enabled)
  468. self.setMinimumWidth(self.get_minimum_table_width())
  469. def table_selection_changed (self):
  470. vm = self.get_selected_vm()
  471. # Update available actions:
  472. self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state)
  473. self.action_resumevm.setEnabled(not vm.last_power_state)
  474. self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0)
  475. self.action_shutdownvm.setEnabled(not vm.is_netvm() and vm.last_power_state and vm.qid != 0)
  476. self.action_appmenus.setEnabled(not vm.is_netvm())
  477. self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm()))
  478. def get_minimum_table_width(self):
  479. tbl_W = 0
  480. for (col, w) in enumerate(VmManagerWindow.columns_widths):
  481. if not self.table.isColumnHidden(col):
  482. tbl_W += w
  483. return tbl_W
  484. def closeEvent (self, event):
  485. if event.spontaneous(): # There is something borked in Qt, as the logic here is inverted on X11
  486. self.hide()
  487. event.ignore()
  488. def create_appvm(self):
  489. dialog = NewAppVmDlg()
  490. # Theoretically we should be locking for writing here and unlock
  491. # only after the VM creation finished. But the code would be more messy...
  492. # Instead we lock for writing in the actual worker thread
  493. self.qvm_collection.lock_db_for_reading()
  494. self.qvm_collection.load()
  495. self.qvm_collection.unlock_db()
  496. label_list = QubesVmLabels.values()
  497. label_list.sort(key=lambda l: l.index)
  498. for (i, label) in enumerate(label_list):
  499. dialog.vmlabel.insertItem(i, label.name)
  500. dialog.vmlabel.setItemIcon (i, QIcon(label.icon_path))
  501. template_vm_list = [vm for vm in self.qvm_collection.values() if not vm.internal and vm.is_template()]
  502. default_index = 0
  503. for (i, vm) in enumerate(template_vm_list):
  504. if vm is self.qvm_collection.get_default_template_vm():
  505. default_index = i
  506. dialog.template_name.insertItem(i, vm.name + " (default)")
  507. else:
  508. dialog.template_name.insertItem(i, vm.name)
  509. dialog.template_name.setCurrentIndex(default_index)
  510. dialog.vmname.selectAll()
  511. dialog.vmname.setFocus()
  512. if dialog.exec_():
  513. vmname = str(dialog.vmname.text())
  514. if self.qvm_collection.get_vm_by_name(vmname) is not None:
  515. QMessageBox.warning (None, "Incorrect AppVM Name!", "A VM with the name <b>{0}</b> already exists in the system!".format(vmname))
  516. return
  517. label = label_list[dialog.vmlabel.currentIndex()]
  518. template_vm = template_vm_list[dialog.template_name.currentIndex()]
  519. allow_networking = dialog.allow_networking.isChecked()
  520. thread_monitor = ThreadMonitor()
  521. thread = threading.Thread (target=self.do_create_appvm, args=(vmname, label, template_vm, allow_networking, thread_monitor))
  522. thread.daemon = True
  523. thread.start()
  524. progress = QProgressDialog ("Creating new AppVM <b>{0}</b>...".format(vmname), "", 0, 0)
  525. progress.setCancelButton(None)
  526. progress.setModal(True)
  527. progress.show()
  528. while not thread_monitor.is_finished():
  529. app.processEvents()
  530. time.sleep (0.1)
  531. progress.hide()
  532. if thread_monitor.success:
  533. trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been created.".format(vmname), msecs=3000)
  534. else:
  535. QMessageBox.warning (None, "Error creating AppVM!", "ERROR: {0}".format(thread_monitor.error_msg))
  536. def do_create_appvm (self, vmname, label, template_vm, allow_networking, thread_monitor):
  537. vm = None
  538. try:
  539. self.qvm_collection.lock_db_for_writing()
  540. self.qvm_collection.load()
  541. vm = self.qvm_collection.add_new_appvm(vmname, template_vm, label = label)
  542. vm.create_on_disk(verbose=False)
  543. vm.add_to_xen_storage()
  544. firewall = vm.get_firewall_conf()
  545. firewall["allow"] = allow_networking
  546. firewall["allowDns"] = allow_networking
  547. vm.write_firewall_conf(firewall)
  548. self.qvm_collection.save()
  549. except Exception as ex:
  550. thread_monitor.set_error_msg (str(ex))
  551. if vm:
  552. vm.remove_from_disk()
  553. finally:
  554. self.qvm_collection.unlock_db()
  555. thread_monitor.set_finished()
  556. def get_selected_vm(self):
  557. row_index = self.table.currentRow()
  558. assert self.vms_in_table[row_index] is not None
  559. vm = self.vms_in_table[row_index].vm
  560. return vm
  561. def remove_appvm(self):
  562. vm = self.get_selected_vm()
  563. assert not vm.is_running()
  564. assert not vm.installed_by_rpm
  565. self.qvm_collection.lock_db_for_reading()
  566. self.qvm_collection.load()
  567. self.qvm_collection.unlock_db()
  568. if vm.is_template():
  569. dependent_vms = self.qvm_collection.get_vms_based_on(vm.qid)
  570. if len(dependent_vms) > 0:
  571. QMessageBox.warning (None, "Warning!",
  572. "This Template VM cannot be removed, because there is at least one AppVM that is based on it.<br>"
  573. "<small>If you want to remove this Template VM and all the AppVMs based on it,"
  574. "you should first remove each individual AppVM that uses this template.</small>")
  575. return
  576. reply = QMessageBox.question(None, "VM Removal Confirmation",
  577. "Are you sure you want to remove the VM <b>'{0}'</b>?<br>"
  578. "<small>All data on this VM's private storage will be lost!</small>".format(vm.name),
  579. QMessageBox.Yes | QMessageBox.Cancel)
  580. if reply == QMessageBox.Yes:
  581. thread_monitor = ThreadMonitor()
  582. thread = threading.Thread (target=self.do_remove_vm, args=(vm, thread_monitor))
  583. thread.daemon = True
  584. thread.start()
  585. progress = QProgressDialog ("Removing VM: <b>{0}</b>...".format(vm.name), "", 0, 0)
  586. progress.setCancelButton(None)
  587. progress.setModal(True)
  588. progress.show()
  589. while not thread_monitor.is_finished():
  590. app.processEvents()
  591. time.sleep (0.1)
  592. progress.hide()
  593. if thread_monitor.success:
  594. trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been removed.".format(vm.name), msecs=3000)
  595. else:
  596. QMessageBox.warning (None, "Error removing VM!", "ERROR: {0}".format(thread_monitor.error_msg))
  597. def do_remove_vm (self, vm, thread_monitor):
  598. try:
  599. self.qvm_collection.lock_db_for_writing()
  600. self.qvm_collection.load()
  601. #TODO: the following two conditions should really be checked by qvm_collection.pop() overload...
  602. if vm.is_template() and qvm_collection.default_template_qid == vm.qid:
  603. qvm_collection.default_template_qid = None
  604. if vm.is_netvm() and qvm_collection.default_netvm_qid == vm.qid:
  605. qvm_collection.default_netvm_qid = None
  606. vm.remove_from_xen_storage()
  607. vm.remove_from_disk()
  608. self.qvm_collection.pop(vm.qid)
  609. self.qvm_collection.save()
  610. except Exception as ex:
  611. thread_monitor.set_error_msg (str(ex))
  612. finally:
  613. self.qvm_collection.unlock_db()
  614. thread_monitor.set_finished()
  615. def resume_vm(self):
  616. vm = self.get_selected_vm()
  617. assert not vm.is_running()
  618. if vm.is_paused():
  619. try:
  620. subprocess.check_call (["/usr/sbin/xl", "unpause", vm.name])
  621. except Exception as ex:
  622. QMessageBox.warning (None, "Error unpausing VM!", "ERROR: {0}".format(ex))
  623. return
  624. thread_monitor = ThreadMonitor()
  625. thread = threading.Thread (target=self.do_start_vm, args=(vm, thread_monitor))
  626. thread.daemon = True
  627. thread.start()
  628. trayIcon.showMessage ("Qubes Manager", "Starting '{0}'...".format(vm.name), msecs=3000)
  629. while not thread_monitor.is_finished():
  630. app.processEvents()
  631. time.sleep (0.1)
  632. if thread_monitor.success:
  633. trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been started.".format(vm.name), msecs=3000)
  634. else:
  635. QMessageBox.warning (None, "Error starting VM!", "ERROR: {0}".format(thread_monitor.error_msg))
  636. def do_start_vm(self, vm, thread_monitor):
  637. try:
  638. vm.verify_files()
  639. xid = vm.start()
  640. except Exception as ex:
  641. thread_monitor.set_error_msg(str(ex))
  642. thread_monitor.set_finished()
  643. return
  644. retcode = subprocess.call ([qubes_guid_path, "-d", str(xid), "-c", vm.label.color, "-i", vm.label.icon, "-l", str(vm.label.index)])
  645. if (retcode != 0):
  646. thread_monitor.set_error_msg("Cannot start qubes_guid!")
  647. thread_monitor.set_finished()
  648. def pause_vm(self):
  649. vm = self.get_selected_vm()
  650. assert vm.is_running()
  651. try:
  652. subprocess.check_call (["/usr/sbin/xl", "pause", vm.name])
  653. except Exception as ex:
  654. QMessageBox.warning (None, "Error pausing VM!", "ERROR: {0}".format(ex))
  655. return
  656. def shutdown_vm(self):
  657. vm = self.get_selected_vm()
  658. assert vm.is_running()
  659. reply = QMessageBox.question(None, "VM Shutdown Confirmation",
  660. "Are you sure you want to power down the VM <b>'{0}'</b>?<br>"
  661. "<small>This will shutdown all the running applications within this VM.</small>".format(vm.name),
  662. QMessageBox.Yes | QMessageBox.Cancel)
  663. app.processEvents()
  664. if reply == QMessageBox.Yes:
  665. try:
  666. subprocess.check_call (["/usr/sbin/xl", "shutdown", vm.name])
  667. except Exception as ex:
  668. QMessageBox.warning (None, "Error shutting down VM!", "ERROR: {0}".format(ex))
  669. return
  670. trayIcon.showMessage ("Qubes Manager", "VM '{0}' is shutting down...".format(vm.name), msecs=3000)
  671. self.shutdown_monitor[vm.qid] = VmShutdownMonitor (vm)
  672. QTimer.singleShot (vm_shutdown_timeout, self.shutdown_monitor[vm.qid].check_if_vm_has_shutdown)
  673. def appmenus_select(self):
  674. vm = self.get_selected_vm()
  675. select_window = AppmenuSelectWindow(vm)
  676. select_window.exec_()
  677. def update_vm(self):
  678. vm = self.get_selected_vm()
  679. assert not vm.is_running()
  680. reply = QMessageBox.question(None, "VM Update Confirmation",
  681. "Are you sure you want to commit template <b>'{0}'</b> changes?<br>"
  682. "<small>AppVMs will see the changes after restart.</small>".format(vm.name),
  683. QMessageBox.Yes | QMessageBox.Cancel)
  684. app.processEvents()
  685. if reply == QMessageBox.Yes:
  686. try:
  687. vm.commit_changes();
  688. except Exception as ex:
  689. QMessageBox.warning (None, "Error commiting changes!", "ERROR: {0}".format(ex))
  690. return
  691. trayIcon.showMessage ("Qubes Manager", "Changes to template '{0}' commited.".format(vm.name), msecs=3000)
  692. def showcpuload(self):
  693. self.__cpugraphs = self.action_showcpuload.isChecked()
  694. self.update_table_columns()
  695. def toggle_inactive_view(self):
  696. self.show_inactive_vms = self.action_showallvms.isChecked()
  697. self.mark_table_for_update()
  698. self.update_table(out_of_schedule = True)
  699. def edit_fw_rules(self):
  700. vm = self.get_selected_vm()
  701. dialog = EditFwRulesDlg()
  702. model = QubesFirewallRulesModel()
  703. model.set_vm(vm)
  704. dialog.set_model(model)
  705. if vm.netvm_vm is not None and not vm.netvm_vm.is_proxyvm():
  706. QMessageBox.warning (None, "VM configuration problem!", "The '{0}' AppVM is not network connected to a FirewallVM!<p>".format(vm.name) +\
  707. "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))
  708. if dialog.exec_():
  709. model.apply_rules()
  710. class QubesTrayIcon(QSystemTrayIcon):
  711. def __init__(self, icon):
  712. QSystemTrayIcon.__init__(self, icon)
  713. self.menu = QMenu()
  714. action_showmanager = self.createAction ("Open VM Manager", slot=show_manager, icon="qubes")
  715. action_backup = self.createAction ("Make backup")
  716. action_preferences = self.createAction ("Preferences")
  717. action_set_netvm = self.createAction ("Set default NetVM", icon="networking")
  718. action_sys_info = self.createAction ("System Info", icon="dom0")
  719. action_exit = self.createAction ("Exit", slot=exit_app)
  720. action_backup.setDisabled(True)
  721. action_preferences.setDisabled(True)
  722. action_set_netvm.setDisabled(True)
  723. action_sys_info.setDisabled(True)
  724. self.addActions (self.menu, (action_showmanager, action_backup, action_sys_info, None, action_preferences, action_set_netvm, None, action_exit))
  725. self.setContextMenu(self.menu)
  726. self.connect (self, SIGNAL("activated (QSystemTrayIcon::ActivationReason)"), self.icon_clicked)
  727. def icon_clicked(self, reason):
  728. if reason == QSystemTrayIcon.Context:
  729. # Handle the right click normally, i.e. display the context menu
  730. return
  731. else:
  732. toggle_manager()
  733. def addActions(self, target, actions):
  734. for action in actions:
  735. if action is None:
  736. target.addSeparator()
  737. else:
  738. target.addAction(action)
  739. def createAction(self, text, slot=None, shortcut=None, icon=None,
  740. tip=None, checkable=False, signal="triggered()"):
  741. action = QAction(text, self)
  742. if icon is not None:
  743. action.setIcon(QIcon(":/%s.png" % icon))
  744. if shortcut is not None:
  745. action.setShortcut(shortcut)
  746. if tip is not None:
  747. action.setToolTip(tip)
  748. action.setStatusTip(tip)
  749. if slot is not None:
  750. self.connect(action, SIGNAL(signal), slot)
  751. if checkable:
  752. action.setCheckable(True)
  753. return action
  754. def show_manager():
  755. manager_window.show()
  756. def toggle_manager():
  757. if manager_window.isVisible():
  758. manager_window.hide()
  759. else:
  760. manager_window.show()
  761. manager_window.update_table(True)
  762. def exit_app():
  763. notifier.stop()
  764. app.exit()
  765. # Bases on the original code by:
  766. # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
  767. def handle_exception( exc_type, exc_value, exc_traceback ):
  768. import sys
  769. import os.path
  770. import traceback
  771. filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
  772. filename = os.path.basename( filename )
  773. error = "%s: %s" % ( exc_type.__name__, exc_value )
  774. QMessageBox.critical(None, "Houston, we have a problem...",
  775. "Whoops. A critical error has occured. This is most likely a bug "
  776. "in Qubes Manager.<br><br>"
  777. "<b><i>%s</i></b>" % error +
  778. "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
  779. % ( line, filename ))
  780. #sys.exit(1)
  781. def main():
  782. # Avoid starting more than one instance of the app
  783. lock = QubesDaemonPidfile ("qubes-manager")
  784. if lock.pidfile_exists():
  785. if lock.pidfile_is_stale():
  786. lock.remove_pidfile()
  787. print "Removed stale pidfile (has the previous daemon instance crashed?)."
  788. else:
  789. exit (0)
  790. lock.create_pidfile()
  791. global qubes_host
  792. qubes_host = QubesHost()
  793. global app
  794. app = QApplication(sys.argv)
  795. app.setOrganizationName("The Qubes Project")
  796. app.setOrganizationDomain("http://qubes-os.org")
  797. app.setApplicationName("Qubes VM Manager")
  798. app.setWindowIcon(QIcon(":/qubes.png"))
  799. sys.excepthook = handle_exception
  800. global manager_window
  801. manager_window = VmManagerWindow()
  802. wm = WatchManager()
  803. mask = EventsCodes.OP_FLAGS.get('IN_MODIFY')
  804. global notifier
  805. notifier = ThreadedNotifier(wm, QubesConfigFileWatcher(manager_window.mark_table_for_update))
  806. notifier.start()
  807. wdd = wm.add_watch(qubes_store_filename, mask)
  808. global trayIcon
  809. trayIcon = QubesTrayIcon(QIcon(":/qubes.png"))
  810. trayIcon.show()
  811. app.exec_()
  812. trayIcon = None