394 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			394 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/python3
 | |
| #
 | |
| # The Qubes OS Project, http://www.qubes-os.org
 | |
| #
 | |
| # Copyright (C) 2018  Marta Marczykowska-Górecka
 | |
| # <marmarta@invisiblethingslab.com>
 | |
| #
 | |
| # This program is free software; you can redistribute it and/or
 | |
| # modify it under the terms of the GNU General Public License
 | |
| # as published by the Free Software Foundation; either version 2
 | |
| # of the License, or (at your option) any later version.
 | |
| #
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU Lesser General Public License along
 | |
| # with this program; if not, see <http://www.gnu.org/licenses/>.
 | |
| #
 | |
| #
 | |
| 
 | |
| from qubesadmin import exc
 | |
| 
 | |
| from PyQt5 import QtWidgets, QtGui, QtCore  # pylint: disable=import-error
 | |
| 
 | |
| from . import ui_templatemanager  # pylint: disable=no-name-in-module
 | |
| from . import utils
 | |
| 
 | |
| column_names = ['State', 'Qube', 'Current template', 'New template']
 | |
| 
 | |
| 
 | |
| class TemplateManagerWindow(
 | |
|         ui_templatemanager.Ui_MainWindow, QtWidgets.QMainWindow):
 | |
| 
 | |
|     def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
 | |
|         # pylint: disable=unused-argument
 | |
|         super(TemplateManagerWindow, self).__init__()
 | |
|         self.setupUi(self)
 | |
| 
 | |
|         self.qubes_app = qubes_app
 | |
|         self.qt_app = qt_app
 | |
|         self.dispatcher = dispatcher
 | |
| 
 | |
|         self.rows_in_table = {}
 | |
|         self.templates = []
 | |
|         self.timers = []
 | |
| 
 | |
|         self.prepare_lists()
 | |
|         self.initialize_table_events()
 | |
| 
 | |
|         self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(
 | |
|             self.apply)
 | |
|         self.buttonBox.button(
 | |
|             QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.cancel)
 | |
|         self.buttonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(
 | |
|             self.reset)
 | |
| 
 | |
|         self.change_all_combobox.currentIndexChanged.connect(
 | |
|             self.change_all_changed)
 | |
|         self.clear_selection_button.clicked.connect(self.clear_selection)
 | |
| 
 | |
|         self.vm_list.show()
 | |
| 
 | |
|     def setup_application(self):
 | |
|         self.qt_app.setApplicationName(self.tr("Template Manager"))
 | |
|         self.qt_app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager"))
 | |
| 
 | |
|     def prepare_lists(self):
 | |
|         self.templates = [vm.name for vm in self.qubes_app.domains
 | |
|                           if vm.klass == 'TemplateVM']
 | |
| 
 | |
|         self.change_all_combobox.addItem(self.tr('(select template)'))
 | |
|         for template in self.templates:
 | |
|             self.change_all_combobox.addItem(template)
 | |
| 
 | |
|         vms_with_templates = [vm for vm in self.qubes_app.domains
 | |
|                               if getattr(vm, 'template', None) and
 | |
|                               vm.klass != 'DispVM']
 | |
| 
 | |
|         self.vm_list.setColumnCount(len(column_names))
 | |
|         self.vm_list.setRowCount(len(vms_with_templates))
 | |
| 
 | |
|         row_count = 0
 | |
|         for vm in vms_with_templates:
 | |
|             row = VMRow(vm, row_count, self.vm_list, column_names,
 | |
|                         self.templates)
 | |
|             self.rows_in_table[vm.name] = row
 | |
|             row_count += 1
 | |
| 
 | |
|         self.vm_list.setHorizontalHeaderLabels(
 | |
|             ['', self.tr('Qube'), self.tr('Current'), self.tr('New')])
 | |
|         self.vm_list.resizeColumnsToContents()
 | |
| 
 | |
|     def initialize_table_events(self):
 | |
|         self.vm_list.cellDoubleClicked.connect(self.table_double_click)
 | |
|         self.vm_list.cellClicked.connect(self.table_click)
 | |
| 
 | |
|         self.vm_list.horizontalHeader().sortIndicatorChanged.connect(
 | |
|             self.sorting_changed)
 | |
| 
 | |
|         self.dispatcher.add_handler('domain-pre-start', self.vm_state_changed)
 | |
|         self.dispatcher.add_handler('domain-start-failed',
 | |
|                                     self.vm_state_changed)
 | |
|         self.dispatcher.add_handler('domain-stopped', self.vm_state_changed)
 | |
|         self.dispatcher.add_handler('domain-shutdown', self.vm_state_changed)
 | |
| 
 | |
|         self.dispatcher.add_handler('domain-add', self.vm_added)
 | |
|         self.dispatcher.add_handler('domain-delete', self.vm_removed)
 | |
| 
 | |
|     def vm_added(self, _submitter, _event, vm, **_kwargs):
 | |
|         # unfortunately, a VM just in the moment of creation may not have
 | |
|         # a template it will have in a second - e.g., when cloning
 | |
|         timer = QtCore.QTimer()
 | |
|         timer.setSingleShot(True)
 | |
|         timer.timeout.connect(lambda: self._vm_added(vm, timer))
 | |
|         self.timers.append(timer)
 | |
|         timer.start(1000)  # 1s
 | |
| 
 | |
|     def _vm_added(self, vm_name, timer):
 | |
|         self.timers.remove(timer)
 | |
|         try:
 | |
|             vm = self.qubes_app.domains[vm_name]
 | |
|             if not getattr(vm, 'template', None) or vm.klass == 'DispVM':
 | |
|                 return
 | |
|         except (exc.QubesException, KeyError):
 | |
|             return  # it was a dispVM that crashed on start
 | |
| 
 | |
|         row_no = self.vm_list.rowCount()
 | |
|         self.vm_list.setRowCount(self.vm_list.rowCount() + 1)
 | |
|         row = VMRow(vm, row_no, self.vm_list, column_names,
 | |
|                     self.templates)
 | |
|         self.rows_in_table[vm.name] = row
 | |
|         self.vm_list.show()
 | |
| 
 | |
|     def vm_removed(self, _submitter, _event, **kwargs):
 | |
|         if kwargs['vm'] not in self.rows_in_table:
 | |
|             return
 | |
| 
 | |
|         self.vm_list.removeRow(self.rows_in_table[kwargs['vm']].name_item.row())
 | |
| 
 | |
|     def vm_state_changed(self, vm, event, **_kwargs):
 | |
|         try:
 | |
|             if vm.name not in self.rows_in_table:
 | |
|                 return
 | |
|         except exc.QubesException:
 | |
|             return  # it was a crashing DispVM or closed DispVM
 | |
| 
 | |
|         if event == 'domain-pre-start':
 | |
|             self.rows_in_table[vm.name].vm_state_change(is_running=True)
 | |
|         elif event == 'domain-start-failed':
 | |
|             self.rows_in_table[vm.name].vm_state_change(is_running=False)
 | |
|         elif event == 'domain-stopped':
 | |
|             self.rows_in_table[vm.name].vm_state_change(is_running=False)
 | |
|         elif event == 'domain-shutdown':
 | |
|             self.rows_in_table[vm.name].vm_state_change(is_running=False)
 | |
| 
 | |
|     def sorting_changed(self, index, _order):
 | |
|         # this is very much not perfect, but QTableWidget does not
 | |
|         # want to be sorted on custom widgets
 | |
|         if index == column_names.index('New template') or \
 | |
|                 index == column_names.index('State'):
 | |
|             self.vm_list.horizontalHeader().setSortIndicator(
 | |
|                 -1, QtCore.Qt.AscendingOrder)
 | |
| 
 | |
|     def clear_selection(self):
 | |
|         for row in self.rows_in_table.values():
 | |
|             if row.checkbox:
 | |
|                 row.checkbox.setChecked(False)
 | |
| 
 | |
|     def change_all_changed(self):
 | |
|         if self.change_all_combobox.currentIndex() == 0:
 | |
|             return
 | |
|         selected_template = self.change_all_combobox.currentText()
 | |
| 
 | |
|         for row in self.rows_in_table.values():
 | |
|             if row.checkbox and row.checkbox.isChecked():
 | |
|                 row.new_item.setCurrentIndex(
 | |
|                     row.new_item.findText(selected_template))
 | |
| 
 | |
|         self.change_all_combobox.setCurrentIndex(0)
 | |
| 
 | |
|     def table_double_click(self, row, column):
 | |
|         template_column = column_names.index('Current template')
 | |
| 
 | |
|         if column != template_column:
 | |
|             return
 | |
| 
 | |
|         template_name = self.vm_list.item(row, column).text()
 | |
| 
 | |
|         for row_number in range(0, self.vm_list.rowCount()):
 | |
|             if self.vm_list.item(
 | |
|                     row_number, template_column).text() == template_name:
 | |
|                 checkbox = self.vm_list.cellWidget(
 | |
|                     row_number, column_names.index('State'))
 | |
|                 if checkbox:
 | |
|                     if row_number == row:
 | |
|                         # this is because double click registers as a
 | |
|                         # single click and a double click
 | |
|                         checkbox.setChecked(False)
 | |
|                     else:
 | |
|                         checkbox.setChecked(True)
 | |
| 
 | |
|     def table_click(self, row, column):
 | |
|         if column == column_names.index('New template'):
 | |
|             return
 | |
| 
 | |
|         checkbox = self.vm_list.cellWidget(row, column_names.index('State'))
 | |
|         if not checkbox:
 | |
|             return
 | |
| 
 | |
|         checkbox.setChecked(not checkbox.isChecked())
 | |
| 
 | |
|     def reset(self):
 | |
|         for row in self.rows_in_table.values():
 | |
|             if row.new_item:
 | |
|                 row.new_item.reset_choice()
 | |
|             if row.checkbox:
 | |
|                 row.checkbox.setChecked(False)
 | |
| 
 | |
|     def cancel(self):
 | |
|         self.close()
 | |
| 
 | |
|     def apply(self):
 | |
|         errors = {}
 | |
|         for vm, row in self.rows_in_table.items():
 | |
|             if row.new_item and row.new_item.changed:
 | |
|                 try:
 | |
|                     setattr(self.qubes_app.domains[vm],
 | |
|                             'template', row.new_item.currentText())
 | |
|                 except Exception as ex:  # pylint: disable=broad-except
 | |
|                     errors[vm] = str(ex)
 | |
|         if errors:
 | |
|             error_messages = [vm + ": " + errors[vm] for vm in errors]
 | |
|             QtWidgets.QMessageBox.warning(
 | |
|                 self,
 | |
|                 self.tr("Errors encountered!"),
 | |
|                 self.tr(
 | |
|                     "Errors encountered on template change in the following "
 | |
|                     "qubes: <br> {}.").format("<br> ".join(error_messages)))
 | |
|         self.close()
 | |
| 
 | |
| 
 | |
| class VMNameItem(QtWidgets.QTableWidgetItem):
 | |
|     # pylint: disable=too-few-public-methods
 | |
|     def __init__(self, vm):
 | |
|         super(VMNameItem, self).__init__()
 | |
|         self.vm = vm
 | |
| 
 | |
|         self.setText(self.vm.name)
 | |
|         self.setIcon(QtGui.QIcon.fromTheme(vm.label.icon))
 | |
| 
 | |
| 
 | |
| class StatusItem(QtWidgets.QTableWidgetItem):
 | |
|     def __init__(self, vm):
 | |
|         super(StatusItem, self).__init__()
 | |
|         self.vm = vm
 | |
| 
 | |
|         self.state = None
 | |
| 
 | |
|     def set_state(self, is_running):
 | |
|         self.state = is_running
 | |
| 
 | |
|         if self.state:
 | |
|             self.setIcon(QtGui.QIcon.fromTheme('dialog-warning'))
 | |
|             self.setToolTip(QtCore.QCoreApplication.translate(
 | |
|                 "template-manager", "Cannot change template on a running VM."))
 | |
|         else:
 | |
|             self.setIcon(QtGui.QIcon())
 | |
|             self.setToolTip("")
 | |
| 
 | |
|     def __lt__(self, other):
 | |
|         if self.state == other.state:
 | |
|             return self.vm.name < other.vm.name
 | |
|         return self.state < other.state
 | |
| 
 | |
| 
 | |
| class CurrentTemplateItem(QtWidgets.QTableWidgetItem):
 | |
|     # pylint: disable=too-few-public-methods
 | |
|     def __init__(self, vm):
 | |
|         super(CurrentTemplateItem, self).__init__()
 | |
|         self.vm = vm
 | |
| 
 | |
|         self.setText(self.vm.template.name)
 | |
| 
 | |
|     def __lt__(self, other):
 | |
|         if self.text() == other.text():
 | |
|             return self.vm.name < other.vm.name
 | |
|         return self.text() < other.text()
 | |
| 
 | |
| 
 | |
| class NewTemplateItem(QtWidgets.QComboBox):
 | |
|     def __init__(self, vm, templates, table_widget):
 | |
|         super(NewTemplateItem, self).__init__()
 | |
|         self.vm = vm
 | |
|         self.table_widget = table_widget
 | |
|         self.changed = False
 | |
| 
 | |
|         for template in templates:
 | |
|             self.addItem(template)
 | |
|         self.setCurrentIndex(self.findText(vm.template.name))
 | |
|         self.start_value = self.currentText()
 | |
| 
 | |
|         self.currentIndexChanged.connect(self.choice_changed)
 | |
| 
 | |
|     def choice_changed(self):
 | |
|         if self.currentText() != self.start_value:
 | |
|             self.changed = True
 | |
|             self.setStyleSheet('font-weight: bold')
 | |
|         else:
 | |
|             self.changed = False
 | |
|             self.setStyleSheet('font-weight: normal')
 | |
| 
 | |
|     def reset_choice(self):
 | |
|         self.setCurrentIndex(self.findText(self.start_value))
 | |
| 
 | |
| 
 | |
| class VMRow:
 | |
|     # pylint: disable=too-few-public-methods
 | |
|     def __init__(self, vm, row_no, table_widget, columns, templates):
 | |
|         self.vm = vm
 | |
|         self.table_widget = table_widget
 | |
|         self.templates = templates
 | |
| 
 | |
|         # state
 | |
|         self.state_item = StatusItem(self.vm)
 | |
|         table_widget.setItem(row_no, columns.index('State'), self.state_item)
 | |
|         self.checkbox = QtWidgets.QCheckBox()
 | |
| 
 | |
|         # icon and name
 | |
|         self.name_item = VMNameItem(self.vm)
 | |
|         table_widget.setItem(row_no, columns.index('Qube'), self.name_item)
 | |
| 
 | |
|         # current template
 | |
|         self.current_item = CurrentTemplateItem(self.vm)
 | |
|         table_widget.setItem(row_no, columns.index('Current template'),
 | |
|                              self.current_item)
 | |
| 
 | |
|         # new template
 | |
|         self.dummy_new_item = QtWidgets.QTableWidgetItem(
 | |
|             QtCore.QCoreApplication.translate("TemplateManager",
 | |
|                                               "qube is running"))
 | |
|         self.new_item = NewTemplateItem(self.vm, templates, table_widget)
 | |
| 
 | |
|         table_widget.setItem(row_no, columns.index('New template'),
 | |
|                              self.dummy_new_item)
 | |
| 
 | |
|         self.vm_state_change(utils.is_running(self.vm, False), row_no)
 | |
| 
 | |
|     def vm_state_change(self, is_running, row=None):
 | |
|         self.state_item.set_state(is_running)
 | |
| 
 | |
|         if not row:
 | |
|             row = 0
 | |
|             while row < self.table_widget.rowCount():
 | |
|                 if self.table_widget.item(
 | |
|                         row, column_names.index('Qube')).text() == \
 | |
|                         self.name_item.text():
 | |
|                     break
 | |
|                 row += 1
 | |
| 
 | |
|         # hiding cellWidgets does not work in a qTableWidget
 | |
|         if not is_running:
 | |
|             self.new_item = NewTemplateItem(self.vm, self.templates,
 | |
|                                             self.table_widget)
 | |
|             self.checkbox = QtWidgets.QCheckBox()
 | |
| 
 | |
|             self.table_widget.setCellWidget(
 | |
|                 row, column_names.index('New template'), self.new_item)
 | |
|             self.table_widget.setCellWidget(
 | |
|                 row, column_names.index('State'), self.checkbox)
 | |
|         else:
 | |
|             new_template = self.table_widget.cellWidget(
 | |
|                 row, column_names.index('New template'))
 | |
|             if new_template:
 | |
|                 self.table_widget.removeCellWidget(
 | |
|                     row, column_names.index('New template'))
 | |
|                 self.new_item = None
 | |
| 
 | |
|             checkbox = self.table_widget.cellWidget(
 | |
|                 row, column_names.index('State'))
 | |
|             if checkbox:
 | |
|                 self.table_widget.removeCellWidget(
 | |
|                     row, column_names.index('State'))
 | |
|                 self.checkbox = None
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     utils.run_asynchronous(TemplateManagerWindow)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 | 
