template_manager.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. from qubesadmin import exc
  23. from PyQt5 import QtWidgets, QtGui, QtCore # pylint: disable=import-error
  24. from . import ui_templatemanager # pylint: disable=no-name-in-module
  25. from . import utils
  26. column_names = ['State', 'Qube', 'Current template', 'New template']
  27. class TemplateManagerWindow(
  28. ui_templatemanager.Ui_MainWindow, QtWidgets.QMainWindow):
  29. def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
  30. # pylint: disable=unused-argument
  31. super(TemplateManagerWindow, self).__init__()
  32. self.setupUi(self)
  33. self.qubes_app = qubes_app
  34. self.qt_app = qt_app
  35. self.dispatcher = dispatcher
  36. self.rows_in_table = {}
  37. self.templates = []
  38. self.timers = []
  39. self.prepare_lists()
  40. self.initialize_table_events()
  41. self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(
  42. self.apply)
  43. self.buttonBox.button(
  44. QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.cancel)
  45. self.buttonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(
  46. self.reset)
  47. self.change_all_combobox.currentIndexChanged.connect(
  48. self.change_all_changed)
  49. self.clear_selection_button.clicked.connect(self.clear_selection)
  50. self.vm_list.show()
  51. def prepare_lists(self):
  52. self.templates = [vm.name for vm in self.qubes_app.domains
  53. if vm.klass == 'TemplateVM']
  54. self.change_all_combobox.addItem(self.tr('(select template)'))
  55. for template in self.templates:
  56. self.change_all_combobox.addItem(template)
  57. vms_with_templates = [vm for vm in self.qubes_app.domains
  58. if getattr(vm, 'template', None) and
  59. vm.klass != 'DispVM']
  60. self.vm_list.setColumnCount(len(column_names))
  61. self.vm_list.setRowCount(len(vms_with_templates))
  62. row_count = 0
  63. for vm in vms_with_templates:
  64. row = VMRow(vm, row_count, self.vm_list, column_names,
  65. self.templates)
  66. self.rows_in_table[vm.name] = row
  67. row_count += 1
  68. self.vm_list.setHorizontalHeaderLabels(
  69. ['', self.tr('Qube'), self.tr('Current'), self.tr('New')])
  70. self.vm_list.resizeColumnsToContents()
  71. def initialize_table_events(self):
  72. self.vm_list.cellDoubleClicked.connect(self.table_double_click)
  73. self.vm_list.cellClicked.connect(self.table_click)
  74. self.vm_list.horizontalHeader().sortIndicatorChanged.connect(
  75. self.sorting_changed)
  76. self.dispatcher.add_handler('domain-pre-start', self.vm_state_changed)
  77. self.dispatcher.add_handler('domain-start-failed',
  78. self.vm_state_changed)
  79. self.dispatcher.add_handler('domain-stopped', self.vm_state_changed)
  80. self.dispatcher.add_handler('domain-shutdown', self.vm_state_changed)
  81. self.dispatcher.add_handler('domain-add', self.vm_added)
  82. self.dispatcher.add_handler('domain-delete', self.vm_removed)
  83. def vm_added(self, _submitter, _event, vm, **_kwargs):
  84. # unfortunately, a VM just in the moment of creation may not have
  85. # a template it will have in a second - e.g., when cloning
  86. timer = QtCore.QTimer()
  87. timer.setSingleShot(True)
  88. timer.timeout.connect(lambda: self._vm_added(vm, timer))
  89. self.timers.append(timer)
  90. timer.start(1000) # 1s
  91. def _vm_added(self, vm_name, timer):
  92. self.timers.remove(timer)
  93. try:
  94. vm = self.qubes_app.domains[vm_name]
  95. if not getattr(vm, 'template', None) or vm.klass == 'DispVM':
  96. return
  97. except (exc.QubesException, KeyError):
  98. return # it was a dispVM that crashed on start
  99. row_no = self.vm_list.rowCount()
  100. self.vm_list.setRowCount(self.vm_list.rowCount() + 1)
  101. row = VMRow(vm, row_no, self.vm_list, column_names,
  102. self.templates)
  103. self.rows_in_table[vm.name] = row
  104. self.vm_list.show()
  105. def vm_removed(self, _submitter, _event, **kwargs):
  106. if kwargs['vm'] not in self.rows_in_table:
  107. return
  108. self.vm_list.removeRow(self.rows_in_table[kwargs['vm']].name_item.row())
  109. def vm_state_changed(self, vm, event, **_kwargs):
  110. try:
  111. if vm.name not in self.rows_in_table:
  112. return
  113. except exc.QubesException:
  114. return # it was a crashing DispVM or closed DispVM
  115. if event == 'domain-pre-start':
  116. self.rows_in_table[vm.name].vm_state_change(is_running=True)
  117. elif event == 'domain-start-failed':
  118. self.rows_in_table[vm.name].vm_state_change(is_running=False)
  119. elif event == 'domain-stopped':
  120. self.rows_in_table[vm.name].vm_state_change(is_running=False)
  121. elif event == 'domain-shutdown':
  122. self.rows_in_table[vm.name].vm_state_change(is_running=False)
  123. def sorting_changed(self, index, _order):
  124. # this is very much not perfect, but QTableWidget does not
  125. # want to be sorted on custom widgets
  126. if index == column_names.index('New template') or \
  127. index == column_names.index('State'):
  128. self.vm_list.horizontalHeader().setSortIndicator(
  129. -1, QtCore.Qt.AscendingOrder)
  130. def clear_selection(self):
  131. for row in self.rows_in_table.values():
  132. if row.checkbox:
  133. row.checkbox.setChecked(False)
  134. def change_all_changed(self):
  135. if self.change_all_combobox.currentIndex() == 0:
  136. return
  137. selected_template = self.change_all_combobox.currentText()
  138. for row in self.rows_in_table.values():
  139. if row.checkbox and row.checkbox.isChecked():
  140. row.new_item.setCurrentIndex(
  141. row.new_item.findText(selected_template))
  142. self.change_all_combobox.setCurrentIndex(0)
  143. def table_double_click(self, row, column):
  144. template_column = column_names.index('Current template')
  145. if column != template_column:
  146. return
  147. template_name = self.vm_list.item(row, column).text()
  148. for row_number in range(0, self.vm_list.rowCount()):
  149. if self.vm_list.item(
  150. row_number, template_column).text() == template_name:
  151. checkbox = self.vm_list.cellWidget(
  152. row_number, column_names.index('State'))
  153. if checkbox:
  154. if row_number == row:
  155. # this is because double click registers as a
  156. # single click and a double click
  157. checkbox.setChecked(False)
  158. else:
  159. checkbox.setChecked(True)
  160. def table_click(self, row, column):
  161. if column == column_names.index('New template'):
  162. return
  163. checkbox = self.vm_list.cellWidget(row, column_names.index('State'))
  164. if not checkbox:
  165. return
  166. checkbox.setChecked(not checkbox.isChecked())
  167. def reset(self):
  168. for row in self.rows_in_table.values():
  169. if row.new_item:
  170. row.new_item.reset_choice()
  171. if row.checkbox:
  172. row.checkbox.setChecked(False)
  173. def cancel(self):
  174. self.close()
  175. def apply(self):
  176. errors = {}
  177. for vm, row in self.rows_in_table.items():
  178. if row.new_item and row.new_item.changed:
  179. try:
  180. setattr(self.qubes_app.domains[vm],
  181. 'template', row.new_item.currentText())
  182. except Exception as ex: # pylint: disable=broad-except
  183. errors[vm] = str(ex)
  184. if errors:
  185. error_messages = [vm + ": " + errors[vm] for vm in errors]
  186. QtWidgets.QMessageBox.warning(
  187. self,
  188. self.tr("Errors encountered!"),
  189. self.tr(
  190. "Errors encountered on template change in the following "
  191. "qubes: <br> {}.").format("<br> ".join(error_messages)))
  192. self.close()
  193. class VMNameItem(QtWidgets.QTableWidgetItem):
  194. # pylint: disable=too-few-public-methods
  195. def __init__(self, vm):
  196. super(VMNameItem, self).__init__()
  197. self.vm = vm
  198. self.setText(self.vm.name)
  199. self.setIcon(QtGui.QIcon.fromTheme(vm.label.icon))
  200. class StatusItem(QtWidgets.QTableWidgetItem):
  201. def __init__(self, vm):
  202. super(StatusItem, self).__init__()
  203. self.vm = vm
  204. self.state = None
  205. def set_state(self, is_running):
  206. self.state = is_running
  207. if self.state:
  208. self.setIcon(QtGui.QIcon.fromTheme('dialog-warning'))
  209. self.setToolTip(self.tr("Cannot change template on a running VM."))
  210. else:
  211. self.setIcon(QtGui.QIcon())
  212. self.setToolTip("")
  213. def __lt__(self, other):
  214. if self.state == other.state:
  215. return self.vm.name < other.vm.name
  216. return self.state < other.state
  217. class CurrentTemplateItem(QtWidgets.QTableWidgetItem):
  218. # pylint: disable=too-few-public-methods
  219. def __init__(self, vm):
  220. super(CurrentTemplateItem, self).__init__()
  221. self.vm = vm
  222. self.setText(self.vm.template.name)
  223. def __lt__(self, other):
  224. if self.text() == other.text():
  225. return self.vm.name < other.vm.name
  226. return self.text() < other.text()
  227. class NewTemplateItem(QtWidgets.QComboBox):
  228. def __init__(self, vm, templates, table_widget):
  229. super(NewTemplateItem, self).__init__()
  230. self.vm = vm
  231. self.table_widget = table_widget
  232. self.changed = False
  233. for template in templates:
  234. self.addItem(template)
  235. self.setCurrentIndex(self.findText(vm.template.name))
  236. self.start_value = self.currentText()
  237. self.currentIndexChanged.connect(self.choice_changed)
  238. def choice_changed(self):
  239. if self.currentText() != self.start_value:
  240. self.changed = True
  241. self.setStyleSheet('font-weight: bold')
  242. else:
  243. self.changed = False
  244. self.setStyleSheet('font-weight: normal')
  245. def reset_choice(self):
  246. self.setCurrentIndex(self.findText(self.start_value))
  247. class VMRow:
  248. # pylint: disable=too-few-public-methods
  249. def __init__(self, vm, row_no, table_widget, columns, templates):
  250. self.vm = vm
  251. self.table_widget = table_widget
  252. self.templates = templates
  253. # state
  254. self.state_item = StatusItem(self.vm)
  255. table_widget.setItem(row_no, columns.index('State'), self.state_item)
  256. self.checkbox = QtWidgets.QCheckBox()
  257. # icon and name
  258. self.name_item = VMNameItem(self.vm)
  259. table_widget.setItem(row_no, columns.index('Qube'), self.name_item)
  260. # current template
  261. self.current_item = CurrentTemplateItem(self.vm)
  262. table_widget.setItem(row_no, columns.index('Current template'),
  263. self.current_item)
  264. # new template
  265. self.dummy_new_item = QtWidgets.QTableWidgetItem(
  266. QtCore.QCoreApplication.translate("TemplateManager",
  267. "qube is running"))
  268. self.new_item = NewTemplateItem(self.vm, templates, table_widget)
  269. table_widget.setItem(row_no, columns.index('New template'),
  270. self.dummy_new_item)
  271. self.vm_state_change(self.vm.is_running(), row_no)
  272. def vm_state_change(self, is_running, row=None):
  273. self.state_item.set_state(is_running)
  274. if not row:
  275. row = 0
  276. while row < self.table_widget.rowCount():
  277. if self.table_widget.item(
  278. row, column_names.index('Qube')).text() == \
  279. self.name_item.text():
  280. break
  281. row += 1
  282. # hiding cellWidgets does not work in a qTableWidget
  283. if not is_running:
  284. self.new_item = NewTemplateItem(self.vm, self.templates,
  285. self.table_widget)
  286. self.checkbox = QtWidgets.QCheckBox()
  287. self.table_widget.setCellWidget(
  288. row, column_names.index('New template'), self.new_item)
  289. self.table_widget.setCellWidget(
  290. row, column_names.index('State'), self.checkbox)
  291. else:
  292. new_template = self.table_widget.cellWidget(
  293. row, column_names.index('New template'))
  294. if new_template:
  295. self.table_widget.removeCellWidget(
  296. row, column_names.index('New template'))
  297. self.new_item = None
  298. checkbox = self.table_widget.cellWidget(
  299. row, column_names.index('State'))
  300. if checkbox:
  301. self.table_widget.removeCellWidget(
  302. row, column_names.index('State'))
  303. self.checkbox = None
  304. def main():
  305. utils.run_asynchronous(
  306. QtCore.QCoreApplication.translate("appname", "Template Manager"),
  307. "qubes-manager",
  308. TemplateManagerWindow)
  309. if __name__ == "__main__":
  310. main()