Merge remote-tracking branch 'origin/pr/135'
* origin/pr/135: Fixed bug in handling state changes Improved template manager UI Missing file added Added links to template manager to qube manager Initial template manager
This commit is contained in:
commit
7dfa33b633
9
qubes-template-manager.desktop
Normal file
9
qubes-template-manager.desktop
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Exec=qubes-template-manager
|
||||||
|
Icon=qubes-manager
|
||||||
|
Terminal=false
|
||||||
|
Name=Qubes Template Manager
|
||||||
|
GenericName=Qubes Template Manager
|
||||||
|
StartupNotify=false
|
||||||
|
Categories=System;
|
@ -1041,6 +1041,12 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
|
|||||||
self.qubes_app)
|
self.qubes_app)
|
||||||
global_settings_window.exec_()
|
global_settings_window.exec_()
|
||||||
|
|
||||||
|
# noinspection PyArgumentList
|
||||||
|
@QtCore.pyqtSlot(name='on_action_manage_templates_triggered')
|
||||||
|
def action_manage_templates_triggered(self):
|
||||||
|
# pylint: disable=invalid-name, no-self-use
|
||||||
|
subprocess.check_call('qubes-template-manager')
|
||||||
|
|
||||||
# noinspection PyArgumentList
|
# noinspection PyArgumentList
|
||||||
@QtCore.pyqtSlot(name='on_action_show_network_triggered')
|
@QtCore.pyqtSlot(name='on_action_show_network_triggered')
|
||||||
def action_show_network_triggered(self):
|
def action_show_network_triggered(self):
|
||||||
|
459
qubesmanager/template_manager.py
Normal file
459
qubesmanager/template_manager.py
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
#!/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/>.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import traceback
|
||||||
|
import quamash
|
||||||
|
import asyncio
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
from qubesadmin import Qubes
|
||||||
|
from qubesadmin import exc
|
||||||
|
from qubesadmin import events
|
||||||
|
|
||||||
|
from PyQt4 import QtGui # pylint: disable=import-error
|
||||||
|
from PyQt4 import QtCore # pylint: disable=import-error
|
||||||
|
from PyQt4 import Qt # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
|
from . import ui_templatemanager # pylint: disable=no-name-in-module
|
||||||
|
|
||||||
|
column_names = ['State', 'Qube', 'Current template', 'New template']
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateManagerWindow(
|
||||||
|
ui_templatemanager.Ui_MainWindow, QtGui.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(QtGui.QDialogButtonBox.Ok).clicked.connect(
|
||||||
|
self.apply)
|
||||||
|
self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).clicked.connect(
|
||||||
|
self.cancel)
|
||||||
|
self.buttonBox.button(QtGui.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 prepare_lists(self):
|
||||||
|
self.templates = [vm.name for vm in self.qubes_app.domains
|
||||||
|
if vm.klass == 'TemplateVM']
|
||||||
|
|
||||||
|
self.change_all_combobox.addItem('(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)]
|
||||||
|
|
||||||
|
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(['', 'Qube', 'Current', '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 = Qt.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):
|
||||||
|
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]
|
||||||
|
QtGui.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(QtGui.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(QtGui.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("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(QtGui.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(QtGui.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 = QtGui.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 = QtGui.QTableWidgetItem("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(self.vm.is_running(), 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 = QtGui.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
|
||||||
|
|
||||||
|
# Bases on the original code by:
|
||||||
|
# Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||||
|
|
||||||
|
filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
error = "%s: %s" % (exc_type.__name__, exc_value)
|
||||||
|
|
||||||
|
strace = ""
|
||||||
|
stacktrace = traceback.extract_tb(exc_traceback)
|
||||||
|
while stacktrace:
|
||||||
|
(filename, line, func, txt) = stacktrace.pop()
|
||||||
|
strace += "----\n"
|
||||||
|
strace += "line: %s\n" % txt
|
||||||
|
strace += "func: %s\n" % func
|
||||||
|
strace += "line no.: %d\n" % line
|
||||||
|
strace += "file: %s\n" % filename
|
||||||
|
|
||||||
|
msg_box = QtGui.QMessageBox()
|
||||||
|
msg_box.setDetailedText(strace)
|
||||||
|
msg_box.setIcon(QtGui.QMessageBox.Critical)
|
||||||
|
msg_box.setWindowTitle("Houston, we have a problem...")
|
||||||
|
msg_box.setText("Whoops. A critical error has occured. "
|
||||||
|
"This is most likely a bug in Qubes Manager.<br><br>"
|
||||||
|
"<b><i>%s</i></b>" % error +
|
||||||
|
"<br/>at line <b>%d</b><br/>of file %s.<br/><br/>"
|
||||||
|
% (line, filename))
|
||||||
|
|
||||||
|
msg_box.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
def loop_shutdown():
|
||||||
|
pending = asyncio.Task.all_tasks()
|
||||||
|
for task in pending:
|
||||||
|
with suppress(asyncio.CancelledError):
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
qt_app = QtGui.QApplication(sys.argv)
|
||||||
|
qt_app.setOrganizationName("The Qubes Project")
|
||||||
|
qt_app.setOrganizationDomain("http://qubes-os.org")
|
||||||
|
qt_app.setApplicationName("Qube Manager")
|
||||||
|
qt_app.setWindowIcon(QtGui.QIcon.fromTheme("qubes-manager"))
|
||||||
|
qt_app.lastWindowClosed.connect(loop_shutdown)
|
||||||
|
|
||||||
|
qubes_app = Qubes()
|
||||||
|
|
||||||
|
loop = quamash.QEventLoop(qt_app)
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
dispatcher = events.EventsDispatcher(qubes_app)
|
||||||
|
|
||||||
|
manager_window = TemplateManagerWindow(qt_app, qubes_app, dispatcher)
|
||||||
|
manager_window.show()
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(
|
||||||
|
asyncio.ensure_future(dispatcher.listen_for_events()))
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
loop_shutdown()
|
||||||
|
exc_type, exc_value, exc_traceback = sys.exc_info()[:3]
|
||||||
|
handle_exception(exc_type, exc_value, exc_traceback)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -48,6 +48,7 @@ cp qubes-vm-create.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
|||||||
cp qubes-backup.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
cp qubes-backup.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
||||||
cp qubes-backup-restore.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
cp qubes-backup-restore.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
||||||
cp qubes-qube-manager.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
cp qubes-qube-manager.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
||||||
|
cp qubes-template-manager.desktop $RPM_BUILD_ROOT/usr/share/applications/
|
||||||
|
|
||||||
%post
|
%post
|
||||||
update-desktop-database &> /dev/null || :
|
update-desktop-database &> /dev/null || :
|
||||||
@ -68,6 +69,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
/usr/bin/qubes-backup-restore
|
/usr/bin/qubes-backup-restore
|
||||||
/usr/bin/qubes-qube-manager
|
/usr/bin/qubes-qube-manager
|
||||||
/usr/bin/qubes-log-viewer
|
/usr/bin/qubes-log-viewer
|
||||||
|
/usr/bin/qubes-template-manager
|
||||||
/usr/libexec/qubes-manager/mount_for_backup.sh
|
/usr/libexec/qubes-manager/mount_for_backup.sh
|
||||||
/usr/libexec/qubes-manager/qvm_about.sh
|
/usr/libexec/qubes-manager/qvm_about.sh
|
||||||
|
|
||||||
@ -95,6 +97,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
%{python3_sitelib}/qubesmanager/utils.py
|
%{python3_sitelib}/qubesmanager/utils.py
|
||||||
%{python3_sitelib}/qubesmanager/bootfromdevice.py
|
%{python3_sitelib}/qubesmanager/bootfromdevice.py
|
||||||
%{python3_sitelib}/qubesmanager/device_list.py
|
%{python3_sitelib}/qubesmanager/device_list.py
|
||||||
|
%{python3_sitelib}/qubesmanager/template_manager.py
|
||||||
|
|
||||||
%{python3_sitelib}/qubesmanager/resources_rc.py
|
%{python3_sitelib}/qubesmanager/resources_rc.py
|
||||||
|
|
||||||
@ -112,6 +115,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
%{python3_sitelib}/qubesmanager/ui_informationnotes.py
|
%{python3_sitelib}/qubesmanager/ui_informationnotes.py
|
||||||
%{python3_sitelib}/qubesmanager/ui_qubemanager.py
|
%{python3_sitelib}/qubesmanager/ui_qubemanager.py
|
||||||
%{python3_sitelib}/qubesmanager/ui_devicelist.py
|
%{python3_sitelib}/qubesmanager/ui_devicelist.py
|
||||||
|
%{python3_sitelib}/qubesmanager/ui_templatemanager.py
|
||||||
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.qm
|
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.qm
|
||||||
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.ts
|
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.ts
|
||||||
|
|
||||||
@ -129,6 +133,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
/usr/share/applications/qubes-backup.desktop
|
/usr/share/applications/qubes-backup.desktop
|
||||||
/usr/share/applications/qubes-backup-restore.desktop
|
/usr/share/applications/qubes-backup-restore.desktop
|
||||||
/usr/share/applications/qubes-qube-manager.desktop
|
/usr/share/applications/qubes-qube-manager.desktop
|
||||||
|
/usr/share/applications/qubes-template-manager.desktop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
@CHANGELOG@
|
@CHANGELOG@
|
||||||
|
3
setup.py
3
setup.py
@ -25,6 +25,7 @@ if __name__ == '__main__':
|
|||||||
'qubes-backup = qubesmanager.backup:main',
|
'qubes-backup = qubesmanager.backup:main',
|
||||||
'qubes-backup-restore = qubesmanager.restore:main',
|
'qubes-backup-restore = qubesmanager.restore:main',
|
||||||
'qubes-qube-manager = qubesmanager.qube_manager:main',
|
'qubes-qube-manager = qubesmanager.qube_manager:main',
|
||||||
'qubes-log-viewer = qubesmanager.log_dialog:main'
|
'qubes-log-viewer = qubesmanager.log_dialog:main',
|
||||||
|
'qubes-template-manager = qubesmanager.template_manager:main'
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -244,7 +244,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1100</width>
|
<width>1100</width>
|
||||||
<height>46</height>
|
<height>28</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="contextMenuPolicy">
|
<property name="contextMenuPolicy">
|
||||||
@ -256,6 +256,7 @@
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="action_global_settings"/>
|
<addaction name="action_global_settings"/>
|
||||||
<addaction name="action_show_network"/>
|
<addaction name="action_show_network"/>
|
||||||
|
<addaction name="action_manage_templates"/>
|
||||||
<addaction name="action_backup"/>
|
<addaction name="action_backup"/>
|
||||||
<addaction name="action_restore"/>
|
<addaction name="action_restore"/>
|
||||||
<addaction name="action_exit"/>
|
<addaction name="action_exit"/>
|
||||||
@ -821,6 +822,14 @@
|
|||||||
<string>&Exit Qube Manager</string>
|
<string>&Exit Qube Manager</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_manage_templates">
|
||||||
|
<property name="text">
|
||||||
|
<string>Manage templates for qubes</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Launch a tool that allows multiple templates to be changed at once</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../resources.qrc"/>
|
<include location="../resources.qrc"/>
|
||||||
|
125
ui/templatemanager.ui
Normal file
125
ui/templatemanager.ui
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>574</width>
|
||||||
|
<height>718</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Template Manager</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="sizeConstraint">
|
||||||
|
<enum>QLayout::SetDefaultConstraint</enum>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QTableWidget" name="vm_list">
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::NoSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<property name="gridStyle">
|
||||||
|
<enum>Qt::DotLine</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sortingEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="cornerButtonEnabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||||
|
<number>20</number>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||||
|
<number>4</number>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="horizontalHeaderStretchLastSection">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="verticalHeaderVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Change all selected to:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="change_all_combobox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string><html><head/><body><p>To select all qubes with a given template, double-click the template name in any row.</p><p><span style=" font-weight:600;">IMPORTANT</span>: Changes will be applied only when you click OK.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="clear_selection_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Clear Selection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
Loading…
Reference in New Issue
Block a user