template_manager.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. #!/usr/bin/python3
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2018 Marta Marczykowska-Górecka
  6. # <marmarta@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License
  10. # as published by the Free Software Foundation; either version 2
  11. # of the License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. #
  21. #
  22. import sys
  23. import os
  24. import os.path
  25. import traceback
  26. import quamash
  27. import asyncio
  28. from contextlib import suppress
  29. from qubesadmin import Qubes
  30. from qubesadmin import exc
  31. from qubesadmin import events
  32. from PyQt4 import QtGui # pylint: disable=import-error
  33. from PyQt4 import QtCore # pylint: disable=import-error
  34. from PyQt4 import Qt # pylint: disable=import-error
  35. import ui_templatemanager # pylint: disable=no-name-in-module
  36. column_names = ['Qube', 'State', 'Current template', 'New template']
  37. class TemplateManagerWindow(
  38. ui_templatemanager.Ui_MainWindow, QtGui.QMainWindow):
  39. def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
  40. # pylint: disable=unused-argument
  41. super(TemplateManagerWindow, self).__init__()
  42. self.setupUi(self)
  43. self.qubes_app = qubes_app
  44. self.qt_app = qt_app
  45. self.dispatcher = dispatcher
  46. self.rows_in_table = {}
  47. self.templates = []
  48. self.timers = []
  49. self.prepare_vm_list()
  50. self.initialize_table_events()
  51. self.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect(
  52. self.apply)
  53. self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).clicked.connect(
  54. self.cancel)
  55. self.buttonBox.button(QtGui.QDialogButtonBox.Reset).clicked.connect(
  56. self.reset)
  57. self.vm_list.show()
  58. def prepare_vm_list(self):
  59. self.templates = [vm.name for vm in self.qubes_app.domains
  60. if vm.klass == 'TemplateVM']
  61. vms_with_templates = [vm for vm in self.qubes_app.domains
  62. if getattr(vm, 'template', None)]
  63. self.vm_list.setColumnCount(len(column_names))
  64. self.vm_list.setRowCount(len(vms_with_templates))
  65. row_count = 0
  66. for vm in vms_with_templates:
  67. row = VMRow(vm, row_count, self.vm_list, column_names,
  68. self.templates)
  69. self.rows_in_table[vm.name] = row
  70. row_count += 1
  71. self.vm_list.setHorizontalHeaderLabels(['Qube', '', 'Current', 'New'])
  72. self.vm_list.resizeColumnsToContents()
  73. def initialize_table_events(self):
  74. self.vm_list.cellDoubleClicked.connect(self.table_double_click)
  75. self.vm_list.horizontalHeader().sortIndicatorChanged.connect(
  76. self.sorting_changed)
  77. self.dispatcher.add_handler('domain-pre-start', self.vm_state_changed)
  78. self.dispatcher.add_handler('domain-start-failed',
  79. self.vm_state_changed)
  80. self.dispatcher.add_handler('domain-stopped', self.vm_state_changed)
  81. self.dispatcher.add_handler('domain-shutdown', self.vm_state_changed)
  82. self.dispatcher.add_handler('domain-add', self.vm_added)
  83. self.dispatcher.add_handler('domain-delete', self.vm_removed)
  84. def vm_added(self, _submitter, _event, vm, **_kwargs):
  85. # unfortunately, a VM just in the moment of creation may not have
  86. # a template it will have in a second - e.g., when cloning
  87. timer = Qt.QTimer()
  88. timer.setSingleShot(True)
  89. timer.timeout.connect(lambda: self._vm_added(vm, timer))
  90. self.timers.append(timer)
  91. timer.start(1000) # 1s
  92. def _vm_added(self, vm_name, timer):
  93. self.timers.remove(timer)
  94. try:
  95. vm = self.qubes_app.domains[vm_name]
  96. if not getattr(vm, 'template', None):
  97. return
  98. except (exc.QubesException, KeyError):
  99. return # it was a dispVM that crashed on start
  100. row_no = self.vm_list.rowCount()
  101. self.vm_list.setRowCount(self.vm_list.rowCount() + 1)
  102. row = VMRow(vm, row_no, self.vm_list, column_names,
  103. self.templates)
  104. self.rows_in_table[vm.name] = row
  105. self.vm_list.show()
  106. def vm_removed(self, _submitter, _event, **kwargs):
  107. if kwargs['vm'] not in self.rows_in_table:
  108. return
  109. self.vm_list.removeRow(self.rows_in_table[kwargs['vm']].name_item.row())
  110. def vm_state_changed(self, vm, event, **_kwargs):
  111. try:
  112. if vm.name not in self.rows_in_table:
  113. return
  114. except exc.QubesException:
  115. return # it was a crashing DispVM or closed DispVM
  116. if event == 'domain-pre-start':
  117. self.rows_in_table[vm.name].vm_state_change(is_running=True)
  118. elif event == 'domain-start-failed':
  119. self.rows_in_table[vm.name].vm_state_change(is_running=False)
  120. elif event == 'domain-stopped':
  121. self.rows_in_table[vm.name].vm_state_change(is_running=False)
  122. elif event == 'domain-shutdown':
  123. self.rows_in_table[vm.name].vm_state_change(is_running=False)
  124. def sorting_changed(self, index, _order):
  125. # this is very much not perfect, but QTableWidget does not
  126. # want to be sorted on custom widgets
  127. # possible fix - try to set data of dummy items.
  128. if index == column_names.index('New template'):
  129. self.vm_list.horizontalHeader().setSortIndicator(
  130. -1, QtCore.Qt.AscendingOrder)
  131. def table_double_click(self, row, column):
  132. template_column = column_names.index('Current template')
  133. if column != template_column:
  134. return
  135. template_name = self.vm_list.item(row, column).text()
  136. self.vm_list.clearSelection()
  137. for row_number in range(0, self.vm_list.rowCount()):
  138. if self.vm_list.item(
  139. row_number, template_column).text() == template_name:
  140. self.vm_list.selectRow(row_number)
  141. def reset(self):
  142. for row in self.rows_in_table.values():
  143. row.new_item.reset_choice()
  144. def cancel(self):
  145. self.close()
  146. def apply(self):
  147. errors = {}
  148. for vm, row in self.rows_in_table.items():
  149. if row.new_item.changed:
  150. try:
  151. setattr(self.qubes_app.domains[vm],
  152. 'template', row.new_item.currentText())
  153. except Exception as ex: # pylint: disable=broad-except
  154. errors[vm] = str(ex)
  155. if errors:
  156. error_messages = [vm + ": " + errors[vm] for vm in errors]
  157. QtGui.QMessageBox.warning(
  158. self,
  159. self.tr("Errors encountered!"),
  160. self.tr(
  161. "Errors encountered on template change in the following "
  162. "qubes: <br> {}.").format("<br> ".join(error_messages)))
  163. self.close()
  164. class VMNameItem(QtGui.QTableWidgetItem):
  165. # pylint: disable=too-few-public-methods
  166. def __init__(self, vm):
  167. super(VMNameItem, self).__init__()
  168. self.vm = vm
  169. self.setText(self.vm.name)
  170. self.setIcon(QtGui.QIcon.fromTheme(vm.label.icon))
  171. class StatusItem(QtGui.QTableWidgetItem):
  172. def __init__(self, vm):
  173. super(StatusItem, self).__init__()
  174. self.vm = vm
  175. self.state = None
  176. def set_state(self, is_running):
  177. self.state = is_running
  178. if self.state:
  179. self.setIcon(QtGui.QIcon.fromTheme('dialog-warning'))
  180. self.setToolTip("Cannot change template on a running VM.")
  181. else:
  182. self.setIcon(QtGui.QIcon())
  183. self.setToolTip("")
  184. def __lt__(self, other):
  185. if self.state == other.state:
  186. return self.vm.name < other.vm.name
  187. return self.state < other.state
  188. class CurrentTemplateItem(QtGui.QTableWidgetItem):
  189. # pylint: disable=too-few-public-methods
  190. def __init__(self, vm):
  191. super(CurrentTemplateItem, self).__init__()
  192. self.vm = vm
  193. self.setText(self.vm.template.name)
  194. def __lt__(self, other):
  195. if self.text() == other.text():
  196. return self.vm.name < other.vm.name
  197. return self.text() < other.text()
  198. class NewTemplateItem(QtGui.QComboBox):
  199. def __init__(self, vm, templates, table_widget):
  200. super(NewTemplateItem, self).__init__()
  201. self.vm = vm
  202. self.table_widget = table_widget
  203. self.changed = False
  204. for template in templates:
  205. self.addItem(template)
  206. self.setCurrentIndex(self.findText(vm.template.name))
  207. self.start_value = self.currentText()
  208. self.currentIndexChanged.connect(self.choice_changed)
  209. def choice_changed(self):
  210. if self.currentText() != self.start_value:
  211. self.changed = True
  212. self.setStyleSheet('font-weight: bold')
  213. else:
  214. self.changed = False
  215. self.setStyleSheet('font-weight: normal')
  216. for row_index in self.table_widget.selectionModel().selectedRows():
  217. widget = self.table_widget.cellWidget(
  218. row_index.row(), column_names.index('New template'))
  219. if widget.isEnabled() and widget.currentText() !=\
  220. self.currentText():
  221. widget.setCurrentIndex(widget.findText(self.currentText()))
  222. self.table_widget.clearSelection()
  223. def reset_choice(self):
  224. self.setCurrentIndex(self.findText(self.start_value))
  225. class VMRow:
  226. # pylint: disable=too-few-public-methods
  227. def __init__(self, vm, row_no, table_widget, columns, templates):
  228. self.vm = vm
  229. # icon and name
  230. self.name_item = VMNameItem(self.vm)
  231. table_widget.setItem(row_no, columns.index('Qube'), self.name_item)
  232. # state
  233. self.state_item = StatusItem(self.vm)
  234. table_widget.setItem(row_no, columns.index('State'), self.state_item)
  235. # current template
  236. self.current_item = CurrentTemplateItem(self.vm)
  237. table_widget.setItem(row_no, columns.index('Current template'),
  238. self.current_item)
  239. # new template
  240. # this is needed to make the cell correctly selectable/non-selectable
  241. self.dummy_new_item = QtGui.QTableWidgetItem()
  242. self.new_item = NewTemplateItem(self.vm, templates, table_widget)
  243. table_widget.setCellWidget(row_no, columns.index('New template'),
  244. self.new_item)
  245. table_widget.setItem(row_no, columns.index('New template'),
  246. self.dummy_new_item)
  247. self.vm_state_change(self.vm.is_running())
  248. def vm_state_change(self, is_running):
  249. self.new_item.setEnabled(not is_running)
  250. self.state_item.set_state(is_running)
  251. items = [self.name_item, self.state_item, self.current_item,
  252. self.dummy_new_item]
  253. for item in items:
  254. if is_running:
  255. item.setFlags(item.flags() & ~QtCore.Qt.ItemIsSelectable)
  256. else:
  257. item.setFlags(item.flags() | QtCore.Qt.ItemIsSelectable)
  258. # Bases on the original code by:
  259. # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
  260. def handle_exception(exc_type, exc_value, exc_traceback):
  261. filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
  262. filename = os.path.basename(filename)
  263. error = "%s: %s" % (exc_type.__name__, exc_value)
  264. strace = ""
  265. stacktrace = traceback.extract_tb(exc_traceback)
  266. while stacktrace:
  267. (filename, line, func, txt) = stacktrace.pop()
  268. strace += "----\n"
  269. strace += "line: %s\n" % txt
  270. strace += "func: %s\n" % func
  271. strace += "line no.: %d\n" % line
  272. strace += "file: %s\n" % filename
  273. msg_box = QtGui.QMessageBox()
  274. msg_box.setDetailedText(strace)
  275. msg_box.setIcon(QtGui.QMessageBox.Critical)
  276. msg_box.setWindowTitle("Houston, we have a problem...")
  277. msg_box.setText("Whoops. A critical error has occured. "
  278. "This is most likely a bug in Qubes Manager.<br><br>"
  279. "<b><i>%s</i></b>" % error +
  280. "<br/>at line <b>%d</b><br/>of file %s.<br/><br/>"
  281. % (line, filename))
  282. msg_box.exec_()
  283. def loop_shutdown():
  284. pending = asyncio.Task.all_tasks()
  285. for task in pending:
  286. with suppress(asyncio.CancelledError):
  287. task.cancel()
  288. def main():
  289. qt_app = QtGui.QApplication(sys.argv)
  290. qt_app.setOrganizationName("The Qubes Project")
  291. qt_app.setOrganizationDomain("http://qubes-os.org")
  292. qt_app.setApplicationName("Qube Manager")
  293. qt_app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager"))
  294. qt_app.lastWindowClosed.connect(loop_shutdown)
  295. qubes_app = Qubes()
  296. loop = quamash.QEventLoop(qt_app)
  297. asyncio.set_event_loop(loop)
  298. dispatcher = events.EventsDispatcher(qubes_app)
  299. manager_window = TemplateManagerWindow(qt_app, qubes_app, dispatcher)
  300. manager_window.show()
  301. try:
  302. loop.run_until_complete(
  303. asyncio.ensure_future(dispatcher.listen_for_events()))
  304. except asyncio.CancelledError:
  305. pass
  306. except Exception: # pylint: disable=broad-except
  307. loop_shutdown()
  308. exc_type, exc_value, exc_traceback = sys.exc_info()[:3]
  309. handle_exception(exc_type, exc_value, exc_traceback)
  310. if __name__ == "__main__":
  311. main()