main.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  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. import os
  24. from PyQt4.QtCore import *
  25. from PyQt4.QtGui import *
  26. from qubes.qubes import QubesVmCollection
  27. from qubes.qubes import QubesException
  28. from qubes.qubes import qubes_store_filename
  29. from qubes.qubes import QubesVmLabels
  30. from qubes.qubes import dry_run
  31. from qubes.qubes import qubes_guid_path
  32. from qubes.qubes import QubesDaemonPidfile
  33. from qubes.qubes import QubesHost
  34. from qubes import qubesutils
  35. import qubesmanager.resources_rc
  36. import ui_newappvmdlg
  37. from ui_mainwindow import *
  38. from appmenu_select import AppmenuSelectWindow
  39. from settings import VMSettingsWindow
  40. from restore import RestoreVMsWindow
  41. from backup import BackupVMsWindow
  42. from global_settings import GlobalSettingsWindow
  43. from firewall import EditFwRulesDlg, QubesFirewallRulesModel
  44. from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
  45. import subprocess
  46. import time
  47. import threading
  48. from datetime import datetime,timedelta
  49. updates_stat_file = 'last_update.stat'
  50. qubes_guid_path = '/usr/bin/qubes_guid'
  51. update_suggestion_interval = 14 # 14 days
  52. class QubesConfigFileWatcher(ProcessEvent):
  53. def __init__ (self, update_func):
  54. self.update_func = update_func
  55. def process_IN_MODIFY (self, event):
  56. self.update_func()
  57. class VmStatusIcon(QLabel):
  58. def __init__(self, vm, parent=None):
  59. super (VmStatusIcon, self).__init__(parent)
  60. self.vm = vm
  61. (icon_pixmap, icon_sz) = self.set_vm_icon(self.vm)
  62. self.setPixmap (icon_pixmap)
  63. self.setFixedSize (icon_sz)
  64. self.previous_power_state = vm.last_power_state
  65. def update(self):
  66. if self.previous_power_state != self.vm.last_power_state:
  67. (icon_pixmap, icon_sz) = self.set_vm_icon(self.vm)
  68. self.setPixmap (icon_pixmap)
  69. self.setFixedSize (icon_sz)
  70. self.previous_power_state = self.vm.last_power_state
  71. def set_vm_icon(self, vm):
  72. if vm.qid == 0:
  73. icon = QIcon (":/dom0.png")
  74. elif vm.is_appvm():
  75. icon = QIcon (vm.label.icon_path)
  76. elif vm.is_template():
  77. icon = QIcon (":/templatevm.png")
  78. elif vm.is_netvm():
  79. icon = QIcon (":/netvm.png")
  80. else:
  81. icon = QIcon()
  82. icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.8)
  83. if vm.last_power_state:
  84. icon_pixmap = icon.pixmap(icon_sz)
  85. else:
  86. icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled)
  87. return (icon_pixmap, icon_sz)
  88. class VmInfoWidget (QWidget):
  89. def __init__(self, vm, parent = None):
  90. super (VmInfoWidget, self).__init__(parent)
  91. layout = QHBoxLayout ()
  92. self.label_name = QLabel (vm.name)
  93. self.vm_icon = VmStatusIcon(vm)
  94. layout.addWidget(self.vm_icon)
  95. layout.addSpacing (10)
  96. layout.addWidget(self.label_name, alignment=Qt.AlignLeft)
  97. self.setLayout(layout)
  98. def update_vm_state (self, vm):
  99. self.vm_icon.update()
  100. class VmTemplateWidget (QWidget):
  101. def __init__(self, vm, parent=None):
  102. super(VmTemplateWidget, self).__init__(parent)
  103. layout = QVBoxLayout()
  104. if vm.template_vm is not None:
  105. self.label_tmpl = QLabel ("<font color=\"black\">" + (vm.template_vm.name) + "</font>")
  106. else:
  107. if vm.is_appvm(): # and vm.template_vm is None
  108. self.label_tmpl = QLabel ("<i><font color=\"gray\">StandaloneVM</i></font>")
  109. elif vm.is_template():
  110. self.label_tmpl = QLabel ("<i><font color=\"gray\">TemplateVM</i></font>")
  111. elif vm.qid == 0:
  112. self.label_tmpl = QLabel ("<i><font color=\"gray\">AdminVM</i></font>")
  113. elif vm.is_netvm():
  114. self.label_tmpl = QLabel ("<i><font color=\"gray\">NetVM</i></font>")
  115. else:
  116. self.label_tmpl = QLabel ("<i><font color=\"gray\">---</i></font>")
  117. layout.addWidget(self.label_tmpl, alignment=Qt.AlignHCenter)
  118. self.setLayout(layout)
  119. class VmIconWidget (QWidget):
  120. def __init__(self, icon_path, enabled=True, parent=None):
  121. super(VmIconWidget, self).__init__(parent)
  122. label_icon = QLabel()
  123. icon = QIcon (icon_path)
  124. icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.8)
  125. icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal)
  126. label_icon.setPixmap (icon_pixmap)
  127. label_icon.setFixedSize (icon_sz)
  128. layout = QVBoxLayout()
  129. layout.addWidget(label_icon)
  130. self.setLayout(layout)
  131. class VmNetvmWidget (QWidget):
  132. def __init__(self, vm, parent=None):
  133. super(VmNetvmWidget, self).__init__(parent)
  134. layout = QHBoxLayout()
  135. self.icon = VmIconWidget(":/networking.png", vm.is_networked())
  136. if vm.is_netvm():
  137. self.label_nvm = QLabel ("<font color=\"black\">self</font>")
  138. elif vm.netvm_vm is not None:
  139. self.label_nvm = QLabel ("<font color=\"black\">" + (vm.netvm_vm.name) + "</font>")
  140. else:
  141. self.label_nvm = QLabel ("<font color=\"black\">None</font>")
  142. layout.addWidget(self.icon, alignment=Qt.AlignLeft)
  143. layout.addWidget(self.label_nvm, alignment=Qt.AlignHCenter)
  144. self.setLayout(layout)
  145. class VmUsageBarWidget (QWidget):
  146. def __init__(self, min, max, format, update_func, vm, load, parent = None):
  147. super (VmUsageBarWidget, self).__init__(parent)
  148. self.min = min
  149. self.max = max
  150. self.update_func = update_func
  151. self.widget = QProgressBar()
  152. self.widget.setMinimum(min)
  153. self.widget.setMaximum(max)
  154. self.widget.setFormat(format);
  155. self.widget.setStyleSheet(
  156. "QProgressBar:horizontal{ \
  157. border: 1px solid lightblue;\
  158. border-radius: 4px;\
  159. background: white;\
  160. text-align: center;\
  161. }\
  162. QProgressBar::chunk:horizontal {\
  163. background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, stop: 0 hsv(210, 170, 207), stop: 1 white);\
  164. }"
  165. )
  166. layout = QHBoxLayout()
  167. layout.addWidget(self.widget)
  168. self.setLayout(layout)
  169. self.update_load(vm, load)
  170. def update_load(self, vm, load):
  171. self.widget.setValue(self.update_func(vm, load))
  172. class LoadChartWidget (QWidget):
  173. def __init__(self, vm, cpu_load = 0, parent = None):
  174. super (LoadChartWidget, self).__init__(parent)
  175. self.load = cpu_load if vm.last_power_state else 0
  176. assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  177. self.load_history = [self.load]
  178. def update_load (self, vm, cpu_load):
  179. self.load = cpu_load if vm.last_power_state else 0
  180. assert self.load >= 0, "load = {0}".format(self.load)
  181. # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  182. if self.load > 100:
  183. # FIXME: This is an ugly workaround :/
  184. self.load = 100
  185. self.load_history.append (self.load)
  186. self.repaint()
  187. def paintEvent (self, Event = None):
  188. p = QPainter (self)
  189. dx = 4
  190. W = self.width()
  191. H = self.height() - 5
  192. N = len(self.load_history)
  193. if N > W/dx:
  194. tail = N - W/dx
  195. N = W/dx
  196. self.load_history = self.load_history[tail:]
  197. assert len(self.load_history) == N
  198. for i in range (0, N-1):
  199. val = self.load_history[N- i - 1]
  200. hue = 200
  201. sat = 70 + val*(255-70)/100
  202. color = QColor.fromHsv (hue, sat, 255)
  203. pen = QPen (color)
  204. pen.setWidth(dx-1)
  205. p.setPen(pen)
  206. if val > 0:
  207. p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100)
  208. class MemChartWidget (QWidget):
  209. def __init__(self, vm, parent = None):
  210. super (MemChartWidget, self).__init__(parent)
  211. self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0
  212. assert self.load >= 0 and self.load <= 100, "mem = {0}".format(self.load)
  213. self.load_history = [self.load]
  214. def update_load (self, vm):
  215. self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0
  216. assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  217. self.load_history.append (self.load)
  218. self.repaint()
  219. def paintEvent (self, Event = None):
  220. p = QPainter (self)
  221. dx = 4
  222. W = self.width()
  223. H = self.height() - 5
  224. N = len(self.load_history)
  225. if N > W/dx:
  226. tail = N - W/dx
  227. N = W/dx
  228. self.load_history = self.load_history[tail:]
  229. assert len(self.load_history) == N
  230. for i in range (0, N-1):
  231. val = self.load_history[N- i - 1]
  232. hue = 120
  233. sat = 70 + val*(255-70)/100
  234. color = QColor.fromHsv (hue, sat, 255)
  235. pen = QPen (color)
  236. pen.setWidth(dx-1)
  237. p.setPen(pen)
  238. if val > 0:
  239. p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100)
  240. class VmUpdateInfoWidget(QWidget):
  241. def __init__(self, vm, show_text=True, parent = None):
  242. super (VmUpdateInfoWidget, self).__init__(parent)
  243. layout = QHBoxLayout ()
  244. self.show_text = show_text
  245. if self.show_text:
  246. self.label=QLabel("")
  247. layout.addWidget(self.label, alignment=Qt.AlignCenter)
  248. else:
  249. self.icon = QLabel("")
  250. layout.addWidget(self.icon, alignment=Qt.AlignHCenter)
  251. self.setLayout(layout)
  252. self.previous_outdated = False
  253. self.previous_update_recommended = None
  254. def update_outdated(self, vm):
  255. outdated = vm.is_outdated()
  256. if outdated and not self.previous_outdated:
  257. self.update_status_widget("outdated")
  258. self.previous_outdated = outdated
  259. if vm.is_updateable():
  260. update_recommended = self.previous_update_recommended
  261. stat_file = vm.dir_path + '/' + updates_stat_file
  262. if not os.path.exists(stat_file) or \
  263. time.time() - os.path.getmtime(stat_file) > \
  264. update_suggestion_interval * 24 * 3600:
  265. update_recommended = True
  266. else:
  267. update_recommended = False
  268. if not self.show_text and self.previous_update_recommended != False:
  269. self.update_status_widget("ok")
  270. if update_recommended and not self.previous_update_recommended:
  271. self.update_status_widget("update")
  272. self.previous_update_recommended = update_recommended
  273. def update_status_widget(self, state):
  274. if state == "ok":
  275. label_text = ""
  276. icon_path = ":/flag-green.png"
  277. tooltip_text = "VM up to date"
  278. elif state == "update":
  279. label_text = "<font color=\"#CCCC00\">Check updates</font>"
  280. icon_path = ":/flag-yellow.png"
  281. tooltip_text = "Update recommended"
  282. elif state == "outdated":
  283. label_text = "<font color=\"red\">VM outdated</font>"
  284. icon_path = ":/flag-red.png"
  285. tooltip_text = "VM outdated"
  286. if self.show_text:
  287. self.label.setText(label_text)
  288. else:
  289. self.layout().removeWidget(self.icon)
  290. self.icon.deleteLater()
  291. self.icon = VmIconWidget(icon_path, True)
  292. self.icon.setToolTip(tooltip_text)
  293. self.layout().addWidget(self.icon, alignment=Qt.AlignCenter)
  294. class VmBlockDevicesWidget(QWidget):
  295. def __init__(self, vm, parent=None):
  296. super(VmBlockDevicesWidget, self).__init__(parent)
  297. combo = QComboBox()
  298. combo.addItem("USB dummy1")
  299. combo.addItem("USB dummy2")
  300. combo.addItem("USB dummy3")
  301. layout = QVBoxLayout()
  302. layout.addWidget(combo)
  303. self.setLayout(layout)
  304. class VmRowInTable(object):
  305. def __init__(self, vm, row_no, table):
  306. self.vm = vm
  307. self.row_no = row_no
  308. table.setRowHeight (row_no, VmManagerWindow.row_height)
  309. self.info_widget = VmInfoWidget(vm)
  310. table.setCellWidget(row_no, 0, self.info_widget)
  311. self.upd_widget = VmUpdateInfoWidget(vm, False)
  312. table.setCellWidget(row_no, 1, self.upd_widget)
  313. self.template_widget = VmTemplateWidget(vm)
  314. table.setCellWidget(row_no, 2, self.template_widget)
  315. self.netvm_widget = VmNetvmWidget(vm)
  316. table.setCellWidget(row_no, 3, self.netvm_widget)
  317. self.cpu_usage_widget = VmUsageBarWidget(0, 100, "",
  318. lambda vm, val: val if vm.last_power_state else 0, vm, 0)
  319. table.setCellWidget(row_no, 4, self.cpu_usage_widget)
  320. self.load_widget = LoadChartWidget(vm)
  321. table.setCellWidget(row_no, 5, self.load_widget)
  322. self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB",
  323. lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0)
  324. table.setCellWidget(row_no, 6, self.mem_usage_widget)
  325. self.mem_widget = MemChartWidget(vm)
  326. table.setCellWidget(row_no, 7, self.mem_widget)
  327. self.updateinfo_widget = VmUpdateInfoWidget(vm, True)
  328. table.setCellWidget(row_no, 8, self.updateinfo_widget)
  329. self.blockdevices_widget = VmBlockDevicesWidget(vm)
  330. table.setCellWidget(row_no, 9, self.blockdevices_widget)
  331. def update(self, counter, cpu_load = None):
  332. self.info_widget.update_vm_state(self.vm)
  333. if cpu_load is not None:
  334. self.cpu_usage_widget.update_load(self.vm, cpu_load)
  335. self.mem_usage_widget.update_load(self.vm, None)
  336. self.load_widget.update_load(self.vm, cpu_load)
  337. self.mem_widget.update_load(self.vm)
  338. self.updateinfo_widget.update_outdated(self.vm)
  339. self.upd_widget.update_outdated(self.vm)
  340. class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg):
  341. def __init__(self, parent = None):
  342. super (NewAppVmDlg, self).__init__(parent)
  343. self.setupUi(self)
  344. vm_shutdown_timeout = 15000 # in msec
  345. class VmShutdownMonitor(QObject):
  346. def __init__(self, vm):
  347. self.vm = vm
  348. def check_if_vm_has_shutdown(self):
  349. vm = self.vm
  350. vm_start_time = vm.get_start_time()
  351. if not vm.is_running() or (vm_start_time and vm_start_time >= datetime.utcnow() - timedelta(0,vm_shutdown_timeout/1000)):
  352. if vm.is_template():
  353. 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)
  354. return
  355. reply = QMessageBox.question(None, "VM Shutdown",
  356. "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),
  357. "Kill it!", "Wait another {0} seconds...".format(vm_shutdown_timeout/1000))
  358. if reply == 0:
  359. vm.force_shutdown()
  360. else:
  361. QTimer.singleShot (vm_shutdown_timeout, self.check_if_vm_has_shutdown)
  362. class ThreadMonitor(QObject):
  363. def __init__(self):
  364. self.success = True
  365. self.error_msg = None
  366. self.event_finished = threading.Event()
  367. def set_error_msg(self, error_msg):
  368. self.success = False
  369. self.error_msg = error_msg
  370. self.set_finished()
  371. def is_finished(self):
  372. return self.event_finished.is_set()
  373. def set_finished(self):
  374. self.event_finished.set()
  375. class VmManagerWindow(Ui_VmManagerWindow, QMainWindow):
  376. row_height = 30
  377. column_width = 200
  378. max_visible_rows = 7
  379. update_interval = 1000 # in msec
  380. show_inactive_vms = True
  381. columns_indices = { "Name": 0,
  382. "Upd": 1,
  383. "Template": 2,
  384. "NetVM": 3,
  385. "CPU": 4,
  386. "CPU Graph": 5,
  387. "MEM": 6,
  388. "MEM Graph": 7,
  389. "Update Info": 8,
  390. "Block Device": 9 }
  391. def __init__(self, parent=None):
  392. super(VmManagerWindow, self).__init__()
  393. self.setupUi(self)
  394. self.toolbar = self.toolBar
  395. self.qvm_collection = QubesVmCollection()
  396. self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed)
  397. cur_pos = self.pos()
  398. self.table.setColumnWidth(0, self.column_width)
  399. self.setSizeIncrement(QtCore.QSize(200, 30))
  400. self.centralwidget.setSizeIncrement(QtCore.QSize(200, 30))
  401. self.table.setSizeIncrement(QtCore.QSize(200, 30))
  402. self.fill_table()
  403. self.move(cur_pos)
  404. self.table.setColumnHidden( self.columns_indices["NetVM"], True)
  405. self.actionNetVM.setChecked(False)
  406. self.table.setColumnHidden( self.columns_indices["Update Info"], True)
  407. self.actionUpdate_Info.setChecked(False)
  408. self.table.setColumnHidden( self.columns_indices["CPU Graph"], True)
  409. self.actionCPU_Graph.setChecked(False)
  410. self.table.setColumnHidden( self.columns_indices["MEM Graph"], True)
  411. self.actionMEM_Graph.setChecked(False)
  412. self.table.setColumnHidden( self.columns_indices["Block Device"], True)
  413. self.actionBlock_Devices.setChecked(False)
  414. self.table.setColumnWidth(self.columns_indices["Upd"], 50)
  415. #self.table.setFrameShape(QFrame.NoFrame)
  416. self.table.setContentsMargins(0,0,0,0)
  417. self.centralwidget.layout().setContentsMargins(0,0,0,0)
  418. self.layout().setContentsMargins(0,0,0,0)
  419. self.counter = 0
  420. self.shutdown_monitor = {}
  421. self.last_measure_results = {}
  422. self.last_measure_time = time.time()
  423. QTimer.singleShot (self.update_interval, self.update_table)
  424. def show(self):
  425. super(VmManagerWindow, self).show()
  426. self.set_table_geom_height()
  427. self.update_table_columns()
  428. def set_table_geom_height(self):
  429. minH = self.table.horizontalHeader().height() +\
  430. 2*self.table.frameWidth()
  431. #All this sizing is kind of magic, so change it only if you have to
  432. #or if you know what you're doing :)
  433. n = self.table.rowCount();
  434. maxH = minH
  435. if n >= self.max_visible_rows:
  436. minH += self.max_visible_rows*self.row_height
  437. maxH += n*self.row_height
  438. else:
  439. minH += n*self.row_height
  440. maxH = minH
  441. self.centralwidget.setMinimumHeight(minH)
  442. self.centralwidget.setMaximumHeight(maxH)
  443. mainwindow_to_add = self.menubar.height() +\
  444. self.toolbar.height() + \
  445. self.menubar.contentsMargins().top() + self.menubar.contentsMargins().bottom() +\
  446. self.toolbar.contentsMargins().top() + self.toolbar.contentsMargins().bottom()
  447. maxH += mainwindow_to_add
  448. minH += mainwindow_to_add
  449. self.setMaximumHeight(maxH)
  450. self.setMinimumHeight(minH)
  451. def get_vms_list(self):
  452. self.qvm_collection.lock_db_for_reading()
  453. self.qvm_collection.load()
  454. self.qvm_collection.unlock_db()
  455. vms_list = [vm for vm in self.qvm_collection.values()]
  456. for vm in vms_list:
  457. vm.last_power_state = vm.is_running()
  458. no_vms = len (vms_list)
  459. vms_to_display = []
  460. # First, the NetVMs...
  461. for netvm in vms_list:
  462. if netvm.is_netvm():
  463. vms_to_display.append (netvm)
  464. # Now, the templates...
  465. for tvm in vms_list:
  466. if tvm.is_template():
  467. vms_to_display.append (tvm)
  468. label_list = QubesVmLabels.values()
  469. label_list.sort(key=lambda l: l.index)
  470. for label in [label.name for label in label_list]:
  471. for appvm in [vm for vm in vms_list if ((vm.is_appvm() or vm.is_disposablevm()) and vm.label.name == label)]:
  472. vms_to_display.append(appvm)
  473. assert len(vms_to_display) == no_vms
  474. return vms_to_display
  475. def fill_table(self):
  476. #self.table.clear()
  477. vms_list = self.get_vms_list()
  478. self.table.setRowCount(len(vms_list))
  479. vms_in_table = []
  480. row_no = 0
  481. for vm in vms_list:
  482. if (not self.show_inactive_vms) and (not vm.last_power_state):
  483. continue
  484. if vm.internal:
  485. continue
  486. vm_row = VmRowInTable (vm, row_no, self.table)
  487. vms_in_table.append (vm_row)
  488. row_no += 1
  489. self.table.setRowCount(row_no)
  490. self.vms_list = vms_list
  491. self.vms_in_table = vms_in_table
  492. self.reload_table = False
  493. def mark_table_for_update(self):
  494. self.reload_table = True
  495. # When calling update_table() directly, always use out_of_schedule=True!
  496. def update_table(self, out_of_schedule=False):
  497. if manager_window.isVisible():
  498. some_vms_have_changed_power_state = False
  499. for vm in self.vms_list:
  500. state = vm.is_running();
  501. if vm.last_power_state != state:
  502. vm.last_power_state = state
  503. some_vms_have_changed_power_state = True
  504. if self.reload_table or ((not self.show_inactive_vms) and some_vms_have_changed_power_state):
  505. self.fill_table()
  506. if self.counter % 3 == 0 or out_of_schedule:
  507. (self.last_measure_time, self.last_measure_results) = \
  508. qubes_host.measure_cpu_usage(self.last_measure_results,
  509. self.last_measure_time)
  510. for vm_row in self.vms_in_table:
  511. cur_cpu_load = None
  512. if vm_row.vm.get_xid() in self.last_measure_results:
  513. cur_cpu_load = self.last_measure_results[vm_row.vm.xid]['cpu_usage']
  514. else:
  515. cur_cpu_load = 0
  516. vm_row.update(self.counter, cpu_load = cur_cpu_load)
  517. else:
  518. for vm_row in self.vms_in_table:
  519. vm_row.update(self.counter)
  520. #self.table_selection_changed()
  521. if not out_of_schedule:
  522. self.counter += 1
  523. QTimer.singleShot (self.update_interval, self.update_table)
  524. def update_table_columns(self):
  525. table_width = self.table.horizontalHeader().length() +\
  526. self.table.verticalScrollBar().width() + \
  527. 2*self.table.frameWidth() + 1
  528. self.table.setFixedWidth( table_width )
  529. self.centralwidget.setFixedWidth(table_width)
  530. self.setFixedWidth(table_width)
  531. def table_selection_changed (self):
  532. vm = self.get_selected_vm()
  533. # Update available actions:
  534. self.action_settings.setEnabled(True)
  535. self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state)
  536. self.action_resumevm.setEnabled(not vm.last_power_state)
  537. self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0)
  538. self.action_shutdownvm.setEnabled(not vm.is_netvm() and vm.last_power_state and vm.qid != 0)
  539. self.action_appmenus.setEnabled(not vm.is_netvm())
  540. self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm()))
  541. self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0)
  542. def closeEvent (self, event):
  543. if event.spontaneous(): # There is something borked in Qt, as the logic here is inverted on X11
  544. self.hide()
  545. event.ignore()
  546. @pyqtSlot(name='on_action_createvm_triggered')
  547. def action_createvm_triggered(self):
  548. dialog = NewAppVmDlg()
  549. print "Create VM triggered!\n"
  550. # Theoretically we should be locking for writing here and unlock
  551. # only after the VM creation finished. But the code would be more messy...
  552. # Instead we lock for writing in the actual worker thread
  553. self.qvm_collection.lock_db_for_reading()
  554. self.qvm_collection.load()
  555. self.qvm_collection.unlock_db()
  556. label_list = QubesVmLabels.values()
  557. label_list.sort(key=lambda l: l.index)
  558. for (i, label) in enumerate(label_list):
  559. dialog.vmlabel.insertItem(i, label.name)
  560. dialog.vmlabel.setItemIcon (i, QIcon(label.icon_path))
  561. template_vm_list = [vm for vm in self.qvm_collection.values() if not vm.internal and vm.is_template()]
  562. default_index = 0
  563. for (i, vm) in enumerate(template_vm_list):
  564. if vm is self.qvm_collection.get_default_template_vm():
  565. default_index = i
  566. dialog.template_name.insertItem(i, vm.name + " (default)")
  567. else:
  568. dialog.template_name.insertItem(i, vm.name)
  569. dialog.template_name.setCurrentIndex(default_index)
  570. dialog.vmname.selectAll()
  571. dialog.vmname.setFocus()
  572. if dialog.exec_():
  573. vmname = str(dialog.vmname.text())
  574. if self.qvm_collection.get_vm_by_name(vmname) is not None:
  575. QMessageBox.warning (None, "Incorrect AppVM Name!", "A VM with the name <b>{0}</b> already exists in the system!".format(vmname))
  576. return
  577. label = label_list[dialog.vmlabel.currentIndex()]
  578. template_vm = template_vm_list[dialog.template_name.currentIndex()]
  579. allow_networking = dialog.allow_networking.isChecked()
  580. thread_monitor = ThreadMonitor()
  581. thread = threading.Thread (target=self.do_create_appvm, args=(vmname, label, template_vm, allow_networking, thread_monitor))
  582. thread.daemon = True
  583. thread.start()
  584. progress = QProgressDialog ("Creating new AppVM <b>{0}</b>...".format(vmname), "", 0, 0)
  585. progress.setCancelButton(None)
  586. progress.setModal(True)
  587. progress.show()
  588. while not thread_monitor.is_finished():
  589. app.processEvents()
  590. time.sleep (0.1)
  591. progress.hide()
  592. if thread_monitor.success:
  593. trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been created.".format(vmname), msecs=3000)
  594. else:
  595. QMessageBox.warning (None, "Error creating AppVM!", "ERROR: {0}".format(thread_monitor.error_msg))
  596. def do_create_appvm (self, vmname, label, template_vm, allow_networking, thread_monitor):
  597. vm = None
  598. try:
  599. self.qvm_collection.lock_db_for_writing()
  600. self.qvm_collection.load()
  601. vm = self.qvm_collection.add_new_appvm(vmname, template_vm, label = label)
  602. vm.create_on_disk(verbose=False)
  603. firewall = vm.get_firewall_conf()
  604. firewall["allow"] = allow_networking
  605. firewall["allowDns"] = allow_networking
  606. vm.write_firewall_conf(firewall)
  607. self.qvm_collection.save()
  608. except Exception as ex:
  609. thread_monitor.set_error_msg (str(ex))
  610. if vm:
  611. vm.remove_from_disk()
  612. finally:
  613. self.qvm_collection.unlock_db()
  614. thread_monitor.set_finished()
  615. def get_selected_vm(self):
  616. row_index = self.table.currentRow()
  617. assert self.vms_in_table[row_index] is not None
  618. vm = self.vms_in_table[row_index].vm
  619. return vm
  620. @pyqtSlot(name='on_action_removevm_triggered')
  621. def action_removevm_triggered(self):
  622. vm = self.get_selected_vm()
  623. assert not vm.is_running()
  624. assert not vm.installed_by_rpm
  625. self.qvm_collection.lock_db_for_reading()
  626. self.qvm_collection.load()
  627. self.qvm_collection.unlock_db()
  628. if vm.is_template():
  629. dependent_vms = self.qvm_collection.get_vms_based_on(vm.qid)
  630. if len(dependent_vms) > 0:
  631. QMessageBox.warning (None, "Warning!",
  632. "This Template VM cannot be removed, because there is at least one AppVM that is based on it.<br>"
  633. "<small>If you want to remove this Template VM and all the AppVMs based on it,"
  634. "you should first remove each individual AppVM that uses this template.</small>")
  635. return
  636. reply = QMessageBox.question(None, "VM Removal Confirmation",
  637. "Are you sure you want to remove the VM <b>'{0}'</b>?<br>"
  638. "<small>All data on this VM's private storage will be lost!</small>".format(vm.name),
  639. QMessageBox.Yes | QMessageBox.Cancel)
  640. if reply == QMessageBox.Yes:
  641. thread_monitor = ThreadMonitor()
  642. thread = threading.Thread (target=self.do_remove_vm, args=(vm, thread_monitor))
  643. thread.daemon = True
  644. thread.start()
  645. progress = QProgressDialog ("Removing VM: <b>{0}</b>...".format(vm.name), "", 0, 0)
  646. progress.setCancelButton(None)
  647. progress.setModal(True)
  648. progress.show()
  649. while not thread_monitor.is_finished():
  650. app.processEvents()
  651. time.sleep (0.1)
  652. progress.hide()
  653. if thread_monitor.success:
  654. trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been removed.".format(vm.name), msecs=3000)
  655. else:
  656. QMessageBox.warning (None, "Error removing VM!", "ERROR: {0}".format(thread_monitor.error_msg))
  657. def do_remove_vm (self, vm, thread_monitor):
  658. try:
  659. self.qvm_collection.lock_db_for_writing()
  660. self.qvm_collection.load()
  661. #TODO: the following two conditions should really be checked by qvm_collection.pop() overload...
  662. if vm.is_template() and qvm_collection.default_template_qid == vm.qid:
  663. qvm_collection.default_template_qid = None
  664. if vm.is_netvm() and qvm_collection.default_netvm_qid == vm.qid:
  665. qvm_collection.default_netvm_qid = None
  666. vm.remove_from_disk()
  667. self.qvm_collection.pop(vm.qid)
  668. self.qvm_collection.save()
  669. except Exception as ex:
  670. thread_monitor.set_error_msg (str(ex))
  671. finally:
  672. self.qvm_collection.unlock_db()
  673. thread_monitor.set_finished()
  674. @pyqtSlot(name='on_action_resumevm_triggered')
  675. def action_resumevm_triggered(self):
  676. vm = self.get_selected_vm()
  677. assert not vm.is_running()
  678. if vm.is_paused():
  679. try:
  680. subprocess.check_call (["/usr/sbin/xl", "unpause", vm.name])
  681. except Exception as ex:
  682. QMessageBox.warning (None, "Error unpausing VM!", "ERROR: {0}".format(ex))
  683. return
  684. thread_monitor = ThreadMonitor()
  685. thread = threading.Thread (target=self.do_start_vm, args=(vm, thread_monitor))
  686. thread.daemon = True
  687. thread.start()
  688. trayIcon.showMessage ("Qubes Manager", "Starting '{0}'...".format(vm.name), msecs=3000)
  689. while not thread_monitor.is_finished():
  690. app.processEvents()
  691. time.sleep (0.1)
  692. if thread_monitor.success:
  693. trayIcon.showMessage ("Qubes Manager", "VM '{0}' has been started.".format(vm.name), msecs=3000)
  694. else:
  695. QMessageBox.warning (None, "Error starting VM!", "ERROR: {0}".format(thread_monitor.error_msg))
  696. def do_start_vm(self, vm, thread_monitor):
  697. try:
  698. vm.verify_files()
  699. xid = vm.start()
  700. except Exception as ex:
  701. thread_monitor.set_error_msg(str(ex))
  702. thread_monitor.set_finished()
  703. return
  704. retcode = subprocess.call ([qubes_guid_path, "-d", str(xid), "-c", vm.label.color, "-i", vm.label.icon, "-l", str(vm.label.index)])
  705. if (retcode != 0):
  706. thread_monitor.set_error_msg("Cannot start qubes_guid!")
  707. thread_monitor.set_finished()
  708. @pyqtSlot(name='on_action_pausevm_triggered')
  709. def action_pausevm_triggered(self):
  710. vm = self.get_selected_vm()
  711. assert vm.is_running()
  712. try:
  713. subprocess.check_call (["/usr/sbin/xl", "pause", vm.name])
  714. except Exception as ex:
  715. QMessageBox.warning (None, "Error pausing VM!", "ERROR: {0}".format(ex))
  716. return
  717. @pyqtSlot(name='on_action_shutdownvm_triggered')
  718. def action_shutdownvm_triggered(self):
  719. vm = self.get_selected_vm()
  720. assert vm.is_running()
  721. reply = QMessageBox.question(None, "VM Shutdown Confirmation",
  722. "Are you sure you want to power down the VM <b>'{0}'</b>?<br>"
  723. "<small>This will shutdown all the running applications within this VM.</small>".format(vm.name),
  724. QMessageBox.Yes | QMessageBox.Cancel)
  725. app.processEvents()
  726. if reply == QMessageBox.Yes:
  727. try:
  728. subprocess.check_call (["/usr/sbin/xl", "shutdown", vm.name])
  729. except Exception as ex:
  730. QMessageBox.warning (None, "Error shutting down VM!", "ERROR: {0}".format(ex))
  731. return
  732. trayIcon.showMessage ("Qubes Manager", "VM '{0}' is shutting down...".format(vm.name), msecs=3000)
  733. self.shutdown_monitor[vm.qid] = VmShutdownMonitor (vm)
  734. QTimer.singleShot (vm_shutdown_timeout, self.shutdown_monitor[vm.qid].check_if_vm_has_shutdown)
  735. @pyqtSlot(name='on_action_settings_triggered')
  736. def action_settings_triggered(self):
  737. vm = self.get_selected_vm()
  738. settings_window = VMSettingsWindow(vm, 1)
  739. settings_window.exec_()
  740. @pyqtSlot(name='on_action_appmenus_triggered')
  741. def action_appmenus_triggered(self):
  742. vm = self.get_selected_vm()
  743. select_window = AppmenuSelectWindow(vm)
  744. select_window.exec_()
  745. @pyqtSlot(name='on_action_updatevm_triggered')
  746. def action_updatevm_triggered(self):
  747. vm = self.get_selected_vm()
  748. if not vm.is_running():
  749. reply = QMessageBox.question(None, "VM Update Confirmation",
  750. "VM need to be running for update. Do you want to start this VM?<br>",
  751. QMessageBox.Yes | QMessageBox.Cancel)
  752. if reply != QMessageBox.Yes:
  753. return
  754. trayIcon.showMessage ("Qubes Manager", "Starting '{0}'...".format(vm.name), msecs=3000)
  755. app.processEvents()
  756. thread_monitor = ThreadMonitor()
  757. thread = threading.Thread (target=self.do_update_vm, args=(vm, thread_monitor))
  758. thread.daemon = True
  759. thread.start()
  760. while not thread_monitor.is_finished():
  761. app.processEvents()
  762. time.sleep (0.2)
  763. if vm.qid != 0:
  764. if thread_monitor.success:
  765. # gpk-update-viewer was started, don't know if user installs updates, but touch stat file anyway
  766. open(vm.dir_path + '/' + updates_stat_file, 'w').close()
  767. else:
  768. QMessageBox.warning (None, "Error VM update!", "ERROR: {0}".format(thread_monitor.error_msg))
  769. def do_update_vm(self, vm, thread_monitor):
  770. try:
  771. if vm.qid == 0:
  772. subprocess.check_call (["/usr/bin/qvm-dom0-update", "--gui"])
  773. else:
  774. qubesutils.run_in_vm(vm, "user:gpk-update-viewer", verbose=False, autostart=True)
  775. except Exception as ex:
  776. thread_monitor.set_error_msg(str(ex))
  777. thread_monitor.set_finished()
  778. return
  779. thread_monitor.set_finished()
  780. @pyqtSlot(name='on_action_showallvms_triggered')
  781. def action_showallvms_triggered(self):
  782. self.show_inactive_vms = self.action_showallvms.isChecked()
  783. self.mark_table_for_update()
  784. self.update_table(out_of_schedule = True)
  785. self.set_table_geom_height()
  786. @pyqtSlot(name='on_action_editfwrules_triggered')
  787. def action_editfwrules_triggered(self):
  788. vm = self.get_selected_vm()
  789. dialog = EditFwRulesDlg()
  790. model = QubesFirewallRulesModel()
  791. model.set_vm(vm)
  792. dialog.set_model(model)
  793. if vm.netvm_vm is not None and not vm.netvm_vm.is_proxyvm():
  794. QMessageBox.warning (None, "VM configuration problem!", "The '{0}' AppVM is not network connected to a FirewallVM!<p>".format(vm.name) +\
  795. "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))
  796. if dialog.exec_():
  797. model.apply_rules()
  798. @pyqtSlot(name='on_action_global_settings_triggered')
  799. def action_global_settings_triggered(self):
  800. global_settings_window = GlobalSettingsWindow()
  801. global_settings_window.exec_()
  802. @pyqtSlot(name='on_action_restore_triggered')
  803. def action_restore_triggered(self):
  804. restore_window = RestoreVMsWindow()
  805. restore_window.exec_()
  806. @pyqtSlot(name='on_action_backup_triggered')
  807. def action_backup_triggered(self):
  808. backup_window = BackupVMsWindow()
  809. backup_window.exec_()
  810. def showhide_collumn(self, col_num, show):
  811. self.table.setColumnHidden( col_num, not show)
  812. self.update_table_columns()
  813. def on_actionUpd_toggled(self, checked):
  814. self.showhide_collumn( self.columns_indices['Upd'], checked)
  815. def on_actionTemplate_toggled(self, checked):
  816. self.showhide_collumn( self.columns_indices['Template'], checked)
  817. def on_actionNetVM_toggled(self, checked):
  818. self.showhide_collumn( self.columns_indices['NetVM'], checked)
  819. def on_actionCPU_toggled(self, checked):
  820. self.showhide_collumn( self.columns_indices['CPU'], checked)
  821. def on_actionCPU_Graph_toggled(self, checked):
  822. self.showhide_collumn( self.columns_indices['CPU Graph'], checked)
  823. def on_actionMEM_toggled(self, checked):
  824. self.showhide_collumn( self.columns_indices['MEM'], checked)
  825. def on_actionMEM_Graph_toggled(self, checked):
  826. self.showhide_collumn( self.columns_indices['MEM Graph'], checked)
  827. def on_actionUpdate_Info_toggled(self, checked):
  828. self.showhide_collumn( self.columns_indices['Update Info'], checked)
  829. def on_actionBlock_Devices_toggled(self, checked):
  830. self.showhide_collumn( self.columns_indices['Block Device'], checked)
  831. class QubesTrayIcon(QSystemTrayIcon):
  832. def __init__(self, icon):
  833. QSystemTrayIcon.__init__(self, icon)
  834. self.menu = QMenu()
  835. action_showmanager = self.createAction ("Open VM Manager", slot=show_manager, icon="qubes")
  836. action_backup = self.createAction ("Make backup")
  837. action_preferences = self.createAction ("Preferences")
  838. action_set_netvm = self.createAction ("Set default NetVM", icon="networking")
  839. action_sys_info = self.createAction ("System Info", icon="dom0")
  840. action_exit = self.createAction ("Exit", slot=exit_app)
  841. action_backup.setDisabled(True)
  842. action_preferences.setDisabled(True)
  843. action_set_netvm.setDisabled(True)
  844. action_sys_info.setDisabled(True)
  845. self.addActions (self.menu, (action_showmanager, action_backup, action_sys_info, None, action_preferences, action_set_netvm, None, action_exit))
  846. self.setContextMenu(self.menu)
  847. self.connect (self, SIGNAL("activated (QSystemTrayIcon::ActivationReason)"), self.icon_clicked)
  848. def icon_clicked(self, reason):
  849. if reason == QSystemTrayIcon.Context:
  850. # Handle the right click normally, i.e. display the context menu
  851. return
  852. else:
  853. toggle_manager()
  854. def addActions(self, target, actions):
  855. for action in actions:
  856. if action is None:
  857. target.addSeparator()
  858. else:
  859. target.addAction(action)
  860. def createAction(self, text, slot=None, shortcut=None, icon=None,
  861. tip=None, checkable=False, signal="triggered()"):
  862. action = QAction(text, self)
  863. if icon is not None:
  864. action.setIcon(QIcon(":/%s.png" % icon))
  865. if shortcut is not None:
  866. action.setShortcut(shortcut)
  867. if tip is not None:
  868. action.setToolTip(tip)
  869. action.setStatusTip(tip)
  870. if slot is not None:
  871. self.connect(action, SIGNAL(signal), slot)
  872. if checkable:
  873. action.setCheckable(True)
  874. return action
  875. def show_manager():
  876. manager_window.show()
  877. def toggle_manager():
  878. if manager_window.isVisible():
  879. manager_window.hide()
  880. else:
  881. manager_window.show()
  882. manager_window.update_table(True)
  883. def exit_app():
  884. notifier.stop()
  885. app.exit()
  886. # Bases on the original code by:
  887. # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
  888. def handle_exception( exc_type, exc_value, exc_traceback ):
  889. import sys
  890. import os.path
  891. import traceback
  892. filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
  893. filename = os.path.basename( filename )
  894. error = "%s: %s" % ( exc_type.__name__, exc_value )
  895. QMessageBox.critical(None, "Houston, we have a problem...",
  896. "Whoops. A critical error has occured. This is most likely a bug "
  897. "in Qubes Manager.<br><br>"
  898. "<b><i>%s</i></b>" % error +
  899. "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
  900. % ( line, filename ))
  901. #sys.exit(1)
  902. def main():
  903. # Avoid starting more than one instance of the app
  904. lock = QubesDaemonPidfile ("qubes-manager")
  905. if lock.pidfile_exists():
  906. if lock.pidfile_is_stale():
  907. lock.remove_pidfile()
  908. print "Removed stale pidfile (has the previous daemon instance crashed?)."
  909. else:
  910. exit (0)
  911. lock.create_pidfile()
  912. global qubes_host
  913. qubes_host = QubesHost()
  914. global app
  915. app = QApplication(sys.argv)
  916. app.setOrganizationName("The Qubes Project")
  917. app.setOrganizationDomain("http://qubes-os.org")
  918. app.setApplicationName("Qubes VM Manager")
  919. app.setWindowIcon(QIcon(":/qubes.png"))
  920. sys.excepthook = handle_exception
  921. global manager_window
  922. manager_window = VmManagerWindow()
  923. wm = WatchManager()
  924. mask = EventsCodes.OP_FLAGS.get('IN_MODIFY')
  925. global notifier
  926. notifier = ThreadedNotifier(wm, QubesConfigFileWatcher(manager_window.mark_table_for_update))
  927. notifier.start()
  928. wdd = wm.add_watch(qubes_store_filename, mask)
  929. global trayIcon
  930. trayIcon = QubesTrayIcon(QIcon(":/qubes.png"))
  931. trayIcon.show()
  932. app.exec_()
  933. trayIcon = None