Merge remote-tracking branch 'origin/pr/239'

* origin/pr/239:
  Added and fixed tests for CloneVM tool
  Updated Clone VM tool to use new better helper functions
  Changed Clone action in Qube Manager and VM settings to use the new Clone Qube dialog
  Added a Clone VM tool
This commit is contained in:
Marek Marczykowski-Górecki 2020-08-06 05:01:36 +02:00
commit 7fc8c2e9e0
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
12 changed files with 713 additions and 78 deletions

4
debian/install vendored
View File

@ -7,6 +7,7 @@
/usr/bin/qubes-qube-manager
/usr/bin/qubes-log-viewer
/usr/bin/qubes-template-manager
/usr/bin/qubes-vm-clone
/usr/libexec/qubes-manager/mount_for_backup.sh
/usr/libexec/qubes-manager/qvm_about.sh
/usr/libexec/qubes-manager/dsa-4371-update
@ -33,6 +34,7 @@
/usr/lib/*/dist-packages/qubesmanager/bootfromdevice.py
/usr/lib/*/dist-packages/qubesmanager/device_list.py
/usr/lib/*/dist-packages/qubesmanager/template_manager.py
/usr/lib/*/dist-packages/qubesmanager/clone_vm.py
/usr/lib/*/dist-packages/qubesmanager/resources_rc.py
@ -51,6 +53,7 @@
/usr/lib/*/dist-packages/qubesmanager/ui_qubemanager.py
/usr/lib/*/dist-packages/qubesmanager/ui_devicelist.py
/usr/lib/*/dist-packages/qubesmanager/ui_templatemanager.py
/usr/lib/*/dist-packages/qubesmanager/ui_clonevmdlg.py
/usr/lib/*/dist-packages/qubesmanager/i18n/qubesmanager_*.qm
/usr/lib/*/dist-packages/qubesmanager/i18n/qubesmanager_*.ts
@ -62,6 +65,7 @@
/usr/lib/*/dist-packages/qubesmanager/tests/test_qube_manager.py
/usr/lib/*/dist-packages/qubesmanager/tests/test_create_new_vm.py
/usr/lib/*/dist-packages/qubesmanager/tests/test_vm_settings.py
/usr/lib/*/dist-packages/qubesmanager/tests/test_clone_vm.py
/usr/lib/*/dist-packages/qubesmanager-*.egg-info/*

182
qubesmanager/clone_vm.py Normal file
View File

@ -0,0 +1,182 @@
#!/usr/bin/python3
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2020 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 os
import sys
import subprocess
from PyQt5 import QtCore, QtWidgets, QtGui # pylint: disable=import-error
import qubesadmin
import qubesadmin.tools
import qubesadmin.exc
from . import common_threads
from . import utils
from .ui_clonevmdlg import Ui_CloneVMDlg # pylint: disable=import-error
class CloneVMDlg(QtWidgets.QDialog, Ui_CloneVMDlg):
def __init__(self, qtapp, app, parent=None, src_vm=None):
super(CloneVMDlg, self).__init__(parent)
self.setupUi(self)
self.qtapp = qtapp
self.app = app
self.thread = None
self.progress = None
utils.initialize_widget_with_vms(
widget=self.src_vm,
qubes_app=self.app,
filter_function=(lambda vm: vm.klass != 'AdminVM'))
if src_vm and self.src_vm.findText(src_vm.name) > -1:
self.src_vm.setCurrentIndex(self.src_vm.findText(src_vm.name))
utils.initialize_widget_with_labels(widget=self.label,
qubes_app=self.app)
self.update_label()
utils.initialize_widget_with_default(
widget=self.storage_pool,
choices=[(str(pool), pool) for pool in self.app.pools.values()],
add_qubes_default=True,
mark_existing_as_default=True,
default_value=self.app.default_pool)
self.set_clone_name()
self.name.setValidator(QtGui.QRegExpValidator(
QtCore.QRegExp("[a-zA-Z0-9_-]*", QtCore.Qt.CaseInsensitive), None))
self.name.selectAll()
self.name.setFocus()
if src_vm:
self.src_vm.setEnabled(False)
else:
self.src_vm.currentIndexChanged.connect(self.set_clone_name)
self.src_vm.currentIndexChanged.connect(self.update_label)
def reject(self):
self.done(0)
def accept(self):
name = self.name.text()
if name in self.app.domains:
QtWidgets.QMessageBox.warning(
self,
self.tr('Incorrect qube name!'),
self.tr('A qube with the name <b>{}</b> already exists in the '
'system!').format(self.name.text()))
return
label = self.label.currentData()
pool = self.storage_pool.currentData()
if pool is qubesadmin.DEFAULT:
pool = None
src_vm = self.src_vm.currentData()
self.thread = common_threads.CloneVMThread(
src_vm, name, pool=pool, label=label)
self.thread.finished.connect(self.clone_finished)
self.thread.start()
self.progress = QtWidgets.QProgressDialog(
self.tr("Cloning qube <b>{0}</b>...").format(name), "", 0, 0)
self.progress.setCancelButton(None)
self.progress.setModal(True)
self.progress.show()
def set_clone_name(self):
vm_name = self.src_vm.currentText()
name_number = 1
name_format = vm_name + '-clone-%d'
while name_format % name_number in self.app.domains.keys():
name_number += 1
self.name.setText(name_format % name_number)
def update_label(self):
vm_label = self.src_vm.currentData().label
label_idx = self.label.findText(str(vm_label))
if label_idx > -1:
self.label.setCurrentIndex(label_idx)
def clone_finished(self):
self.progress.hide()
if not self.thread.msg_is_success:
QtWidgets.QMessageBox.warning(
self,
self.tr("Error cloning the qube!"),
self.tr("ERROR: {0}").format(self.thread.msg))
else:
(title, msg) = self.thread.msg
QtWidgets.QMessageBox.warning(
self,
title,
msg)
self.done(0)
if self.thread.msg_is_success:
if self.launch_settings.isChecked():
subprocess.check_call(['qubes-vm-settings',
str(self.name.text())])
parser = qubesadmin.tools.QubesArgumentParser(vmname_nargs='?')
def main(args=None):
args = parser.parse_args(args)
if args.domains:
src_vm = args.domains.pop()
else:
src_vm = None
qtapp = QtWidgets.QApplication(sys.argv)
translator = QtCore.QTranslator(qtapp)
locale = QtCore.QLocale.system().name()
i18n_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'i18n')
translator.load("qubesmanager_{!s}.qm".format(locale), i18n_dir)
qtapp.installTranslator(translator)
QtCore.QCoreApplication.installTranslator(translator)
qtapp.setOrganizationName('Invisible Things Lab')
qtapp.setOrganizationDomain('https://www.qubes-os.org/')
qtapp.setApplicationName(QtCore.QCoreApplication.translate(
"appname", 'Clone qube'))
dialog = CloneVMDlg(qtapp, args.app, src_vm=src_vm)
dialog.exec_()

View File

@ -55,13 +55,18 @@ class RemoveVMThread(QubesThread):
# pylint: disable=too-few-public-methods
class CloneVMThread(QubesThread):
def __init__(self, vm, dst_name):
def __init__(self, vm, dst_name, pool=None, label=None):
super(CloneVMThread, self).__init__(vm)
self.dst_name = dst_name
self.pool = pool
self.label = label
def run(self):
try:
self.vm.app.clone_vm(self.vm, self.dst_name)
self.vm.app.clone_vm(self.vm, self.dst_name, pool=self.pool)
if self.label:
result_vm = self.vm.app.domains[self.dst_name]
result_vm.label = self.label
self.msg = (self.tr("Success"),
self.tr("The qube was cloned successfully."))
self.msg_is_success = True

View File

@ -53,6 +53,7 @@ from . import create_new_vm
from . import log_dialog
from . import utils as manager_utils
from . import common_threads
from . import clone_vm
class SearchBox(QLineEdit):
@ -1045,39 +1046,10 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QMainWindow):
def action_clonevm_triggered(self):
for vm_info in self.get_selected_vms():
vm = vm_info.vm
name_number = 1
name_format = vm.name + '-clone-%d'
while name_format % name_number in self.qubes_app.domains.keys():
name_number += 1
(clone_name, ok) = QInputDialog.getText(
self, self.tr('Qubes clone Qube'),
self.tr('Enter name for Qube <b>{}</b> clone:').format(vm.name),
text=(name_format % name_number))
if not ok or clone_name == "":
return
name_in_use = clone_name in self.qubes_app.domains
if name_in_use:
QMessageBox.warning(
self, self.tr("Name already in use!"),
self.tr("There already exists a qube called '{}'. "
"Cloning aborted.").format(clone_name))
return
self.progress = QProgressDialog(
self.tr(
"Cloning Qube..."), "", 0, 0)
self.progress.setCancelButton(None)
self.progress.setModal(True)
self.progress.setWindowTitle(self.tr("Cloning qube..."))
self.progress.show()
thread = common_threads.CloneVMThread(vm, clone_name)
thread.finished.connect(self.clear_threads)
self.threads_list.append(thread)
thread.start()
with common_threads.busy_cursor():
clone_window = clone_vm.CloneVMDlg(
self.qt_app, self.qubes_app, src_vm=vm)
clone_window.exec_()
# noinspection PyArgumentList
@pyqtSlot(name='on_action_resumevm_triggered')

View File

@ -36,6 +36,7 @@ from . import utils
from . import multiselectwidget
from . import common_threads
from . import device_list
from . import clone_vm
from .appmenu_select import AppmenuSelectManager
from . import firewall
@ -653,25 +654,10 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtWidgets.QDialog):
self.tr("The qube will not be removed."))
def clone_vm(self):
cloned_vm_name, ok = QtWidgets.QInputDialog.getText(
self,
self.tr('Clone qube'),
self.tr('Name for the cloned qube:'))
if ok:
thread = common_threads.CloneVMThread(self.vm, cloned_vm_name)
thread.finished.connect(self.clear_threads)
self.threads_list.append(thread)
self.progress = QtWidgets.QProgressDialog(
self.tr("Cloning Qube..."), "", 0, 0)
self.progress.setCancelButton(None)
self.progress.setModal(True)
self.thread_closes = True
self.progress.show()
# TODO: improvement: maybe this can be refactored into less repetition?
thread.start()
with common_threads.busy_cursor():
clone_window = clone_vm.CloneVMDlg(
self.qapp, self.qubesapp, src_vm=self.vm)
clone_window.exec_()
######### advanced tab

View File

@ -0,0 +1,276 @@
#!/usr/bin/python3
#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2020 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 General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
import logging.handlers
import unittest
import unittest.mock
from PyQt5 import QtTest, QtCore
from qubesadmin import Qubes
from qubesmanager.tests import init_qtapp
from qubesmanager import clone_vm
# TODO: test when you do give a src vm
class CloneVMTest(unittest.TestCase):
def setUp(self):
super(CloneVMTest, self).setUp()
self.qtapp, self.loop = init_qtapp()
self.qapp = Qubes()
# mock up the Create VM Thread to avoid changing system state
self.patcher_thread = unittest.mock.patch(
'qubesmanager.common_threads.CloneVMThread')
self.mock_thread = self.patcher_thread.start()
self.addCleanup(self.patcher_thread.stop)
# mock the progress dialog to speed testing up
self.patcher_progress = unittest.mock.patch(
'PyQt5.QtWidgets.QProgressDialog')
self.mock_progress = self.patcher_progress.start()
self.addCleanup(self.patcher_progress.stop)
# mock the progress dialog to speed testing up
self.patcher_warning = unittest.mock.patch(
'PyQt5.QtWidgets.QMessageBox.warning')
self.mock_warning = self.patcher_warning.start()
self.addCleanup(self.patcher_warning.stop)
self.dialog = clone_vm.CloneVMDlg(self.qtapp, self.qapp)
def test_00_window_loads(self):
self.assertGreater(self.dialog.src_vm.count(), 0,
"No source vms shown")
self.assertGreater(self.dialog.label.count(), 0, "No labels listed")
self.assertGreater(self.dialog.storage_pool.count(), 0,
"No pools listed")
self.assertTrue(self.dialog.src_vm.isEnabled(),
"source vm dialog not active")
def test_01_cancel_works(self):
self.__click_cancel()
self.assertEqual(self.mock_thread.call_count, 0,
"Attempted to create VM on cancel")
def test_02_name_correctly_updates(self):
src_name = self.dialog.src_vm.currentText()
target_name = self.dialog.name.text()
self.assertTrue(target_name.startswith(src_name),
"target name does not contain source name")
self.assertTrue('clone' in target_name,
"target name does not contain >clone<")
self.dialog.src_vm.setCurrentIndex(self.dialog.src_vm.currentIndex()+1)
src_name = self.dialog.src_vm.currentText()
target_name = self.dialog.name.text()
self.assertTrue(target_name.startswith(src_name),
"target name does not contain source name")
self.assertTrue('clone' in target_name,
"target name does not contain >clone<")
def test_03_label_correctly_updates(self):
src_label = self.dialog.src_vm.currentData().label.name
target_label = self.dialog.label.currentText()
self.assertEqual(src_label, target_label, "incorrect start label")
while self.dialog.src_vm.currentData().label.name == src_label:
self.dialog.src_vm.setCurrentIndex(
self.dialog.src_vm.currentIndex() + 1)
src_label = self.dialog.src_vm.currentData().label.name
target_label = self.dialog.label.currentText()
self.assertEqual(src_label, target_label,
"label did not change correctly")
def test_04_clone_first_vm(self):
self.dialog.name.setText("clone-test")
src_vm = self.qapp.domains[self.dialog.src_vm.currentText()]
self.__click_ok()
self.mock_thread.assert_called_once_with(
src_vm, "clone-test", pool=None, label=src_vm.label)
self.mock_thread().start.assert_called_once_with()
def test_05_clone_other_vm(self):
self.dialog.src_vm.setCurrentIndex(self.dialog.src_vm.currentIndex()+1)
src_vm = self.qapp.domains[self.dialog.src_vm.currentText()]
dst_name = self.dialog.name.text()
self.__click_ok()
self.mock_thread.assert_called_once_with(
src_vm, dst_name, pool=None, label=src_vm.label)
self.mock_thread().start.assert_called_once_with()
def test_06_clone_label(self):
src_vm = self.qapp.domains[self.dialog.src_vm.currentText()]
dst_name = self.dialog.name.text()
while self.dialog.label.currentText() != 'blue':
self.dialog.label.setCurrentIndex(
self.dialog.label.currentIndex()+1)
self.__click_ok()
self.mock_thread.assert_called_once_with(
src_vm, dst_name, pool=None, label=self.qapp.labels['blue'])
self.mock_thread().start.assert_called_once_with()
@unittest.mock.patch('subprocess.check_call')
def test_07_launch_settings(self, mock_call):
self.dialog.launch_settings.setChecked(True)
self.dialog.name.setText("clone-test")
self.__click_ok()
self.mock_thread.assert_called_once_with(
unittest.mock.ANY, "clone-test", pool=None,
label=unittest.mock.ANY)
self.mock_thread().msg = ("Success", "Success")
self.dialog.clone_finished()
mock_call.assert_called_once_with(['qubes-vm-settings', "clone-test"])
def test_08_progress_hides(self):
self.dialog.name.setText("clone-test")
self.__click_ok()
self.mock_thread.assert_called_once_with(
unittest.mock.ANY, "clone-test", pool=None,
label=unittest.mock.ANY)
# make sure the thread is not reporting an error
self.mock_thread().start.assert_called_once_with()
self.mock_thread().msg = ("Success", "Success")
self.mock_progress().show.assert_called_once_with()
self.dialog.clone_finished()
self.mock_progress().hide.assert_called_once_with()
def test_09_pool_nondefault(self):
while 'default' in self.dialog.storage_pool.currentText():
self.dialog.storage_pool.setCurrentIndex(
self.dialog.storage_pool.currentIndex()+1)
selected_pool = self.dialog.storage_pool.currentText()
self.__click_ok()
self.mock_thread.assert_called_once_with(
unittest.mock.ANY, unittest.mock.ANY,
pool=selected_pool,
label=unittest.mock.ANY)
self.mock_thread().start.assert_called_once_with()
def __click_ok(self):
okwidget = self.dialog.buttonBox.button(
self.dialog.buttonBox.Ok)
QtTest.QTest.mouseClick(okwidget, QtCore.Qt.LeftButton)
def __click_cancel(self):
cancelwidget = self.dialog.buttonBox.button(
self.dialog.buttonBox.Cancel)
QtTest.QTest.mouseClick(cancelwidget, QtCore.Qt.LeftButton)
class CloneVMTestSrcVM(unittest.TestCase):
def setUp(self):
super(CloneVMTestSrcVM, self).setUp()
self.qtapp, self.loop = init_qtapp()
self.qapp = Qubes()
# mock up the Create VM Thread to avoid changing system state
self.patcher_thread = unittest.mock.patch(
'qubesmanager.common_threads.CloneVMThread')
self.mock_thread = self.patcher_thread.start()
self.addCleanup(self.patcher_thread.stop)
# mock the progress dialog to speed testing up
self.patcher_progress = unittest.mock.patch(
'PyQt5.QtWidgets.QProgressDialog')
self.mock_progress = self.patcher_progress.start()
self.addCleanup(self.patcher_progress.stop)
# mock the progress dialog to speed testing up
self.patcher_warning = unittest.mock.patch(
'PyQt5.QtWidgets.QMessageBox.warning')
self.mock_warning = self.patcher_warning.start()
self.addCleanup(self.patcher_warning.stop)
self.src_vm = next(
domain for domain in self.qapp.domains
if domain.klass != 'AdminVM')
self.dialog = clone_vm.CloneVMDlg(self.qtapp, self.qapp,
src_vm=self.src_vm)
def test_00_window_loads(self):
self.assertEqual(self.dialog.src_vm.currentText(), self.src_vm.name)
self.assertEqual(self.dialog.src_vm.currentData(), self.src_vm)
self.assertFalse(self.dialog.src_vm.isEnabled(),
"source vm dialog active")
self.assertEqual(self.dialog.label.currentText(),
self.src_vm.label.name)
def test_01_simple_clone(self):
self.dialog.name.setText("clone-test")
self.__click_ok()
self.mock_thread.assert_called_once_with(
self.src_vm, "clone-test", pool=None, label=self.src_vm.label)
self.mock_thread().start.assert_called_once_with()
def __click_ok(self):
okwidget = self.dialog.buttonBox.button(
self.dialog.buttonBox.Ok)
QtTest.QTest.mouseClick(okwidget, QtCore.Qt.LeftButton)
if __name__ == "__main__":
ha_syslog = logging.handlers.SysLogHandler('/dev/log')
ha_syslog.setFormatter(
logging.Formatter('%(name)s[%(process)d]: %(message)s'))
logging.root.addHandler(ha_syslog)
unittest.main()

View File

@ -22,7 +22,6 @@
import logging.handlers
import unittest
import unittest.mock
import qubesadmin
from PyQt5 import QtTest, QtCore
from qubesadmin import Qubes

View File

@ -637,9 +637,8 @@ class QubeManagerTest(unittest.TestCase):
self.dialog.action_manage_templates.trigger()
mock_subprocess.assert_called_once_with('qubes-template-manager')
@unittest.mock.patch('qubesmanager.common_threads.CloneVMThread')
@unittest.mock.patch('PyQt5.QtWidgets.QInputDialog.getText')
def test_232_clonevm(self, mock_input, mock_thread):
@unittest.mock.patch('qubesmanager.clone_vm.CloneVMDlg')
def test_232_clonevm(self, mock_clone):
action = self.dialog.action_clonevm
self._select_admin_vm()
@ -648,18 +647,9 @@ class QubeManagerTest(unittest.TestCase):
selected_vm = self._select_non_admin_vm()
self.assertTrue(action.isEnabled())
mock_input.return_value = (selected_vm.name + "clone1", False)
action.trigger()
self.assertEqual(mock_thread.call_count, 0,
"Ignores cancelling clone VM")
mock_input.return_value = (selected_vm.name + "clone1", True)
action.trigger()
mock_thread.assert_called_once_with(selected_vm,
selected_vm.name + "clone1")
mock_thread().finished.connect.assert_called_once_with(
self.dialog.clear_threads)
mock_thread().start.assert_called_once_with()
mock_clone.assert_called_once_with(self.qtapp, self.qapp,
src_vm=selected_vm)
def test_233_search_action(self):
self.qtapp.setActiveWindow(self.dialog.searchbox)

View File

@ -311,23 +311,19 @@ class VMSettingsTest(unittest.TestCase):
mock_thread.assert_called_with(self.vm, "test-vm2", unittest.mock.ANY)
mock_thread().start.assert_called_with()
# TODO: thread tests for rename
@unittest.mock.patch('PyQt5.QtWidgets.QProgressDialog')
@unittest.mock.patch('PyQt5.QtWidgets.QInputDialog.getText')
@unittest.mock.patch('qubesmanager.common_threads.CloneVMThread')
def test_12_clone_vm(self, mock_thread, mock_input, _):
@unittest.mock.patch('qubesmanager.clone_vm.CloneVMDlg')
def test_12_clone_vm(self, mock_clone):
self.vm = self.qapp.add_new_vm("AppVM", "test-vm", "blue")
self.dialog = vm_settings.VMSettingsWindow(
self.vm, qapp=self.qtapp, qubesapp=self.qapp, init_page="basic")
self.assertTrue(self.dialog.clone_vm_button.isEnabled())
mock_input.return_value = ("test-vm2", True)
self.dialog.clone_vm_button.click()
mock_thread.assert_called_with(self.vm, "test-vm2")
mock_thread().start.assert_called_with()
mock_clone.assert_called_once_with(self.qtapp, self.qapp,
src_vm=self.vm)
@unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
@unittest.mock.patch('PyQt5.QtWidgets.QProgressDialog')

View File

@ -57,6 +57,7 @@ rm -rf $RPM_BUILD_ROOT
/usr/bin/qubes-global-settings
/usr/bin/qubes-vm-settings
/usr/bin/qubes-vm-create
/usr/bin/qubes-vm-clone
/usr/bin/qubes-vm-boot-from-device
/usr/bin/qubes-backup
/usr/bin/qubes-backup-restore
@ -84,6 +85,7 @@ rm -rf $RPM_BUILD_ROOT
%{python3_sitelib}/qubesmanager/releasenotes.py
%{python3_sitelib}/qubesmanager/informationnotes.py
%{python3_sitelib}/qubesmanager/create_new_vm.py
%{python3_sitelib}/qubesmanager/clone_vm.py
%{python3_sitelib}/qubesmanager/common_threads.py
%{python3_sitelib}/qubesmanager/qube_manager.py
%{python3_sitelib}/qubesmanager/utils.py
@ -108,6 +110,7 @@ rm -rf $RPM_BUILD_ROOT
%{python3_sitelib}/qubesmanager/ui_qubemanager.py
%{python3_sitelib}/qubesmanager/ui_devicelist.py
%{python3_sitelib}/qubesmanager/ui_templatemanager.py
%{python3_sitelib}/qubesmanager/ui_clonevmdlg.py
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.qm
%{python3_sitelib}/qubesmanager/i18n/qubesmanager_*.ts
@ -119,6 +122,7 @@ rm -rf $RPM_BUILD_ROOT
%{python3_sitelib}/qubesmanager/tests/test_qube_manager.py
%{python3_sitelib}/qubesmanager/tests/test_create_new_vm.py
%{python3_sitelib}/qubesmanager/tests/test_vm_settings.py
%{python3_sitelib}/qubesmanager/tests/test_clone_vm.py
%dir %{python3_sitelib}/qubesmanager-*.egg-info
%{python3_sitelib}/qubesmanager-*.egg-info/*

View File

@ -21,6 +21,7 @@ if __name__ == '__main__':
'qubes-global-settings = qubesmanager.global_settings:main',
'qubes-vm-settings = qubesmanager.settings:main',
'qubes-vm-create = qubesmanager.create_new_vm:main',
'qubes-vm-clone = qubesmanager.clone_vm:main',
'qubes-vm-boot-from-device = qubesmanager.bootfromdevice:main',
'qubes-backup = qubesmanager.backup:main',
'qubes-backup-restore = qubesmanager.restore:main',

220
ui/clonevmdlg.ui Normal file
View File

@ -0,0 +1,220 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CloneVMDlg</class>
<widget class="QDialog" name="CloneVMDlg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>616</width>
<height>238</height>
</rect>
</property>
<property name="windowTitle">
<string>Clone qube</string>
</property>
<property name="windowIcon">
<iconset theme="qubes-manager">
<normaloff>.</normaloff>.</iconset>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabWidgetPage1">
<attribute name="title">
<string>Basic</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<property name="horizontalSpacing">
<number>12</number>
</property>
<property name="verticalSpacing">
<number>9</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Source qube:</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QComboBox" name="label">
<property name="frame">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="name">
<property name="text">
<string>my-cloned-qube</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QComboBox" name="src_vm"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="name_label">
<property name="text">
<string>Clone name:</string>
</property>
</widget>
</item>
<item row="5" column="1" colspan="3">
<widget class="QCheckBox" name="launch_settings">
<property name="text">
<string>launch settings after cloning</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Advanced</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<property name="horizontalSpacing">
<number>12</number>
</property>
<property name="verticalSpacing">
<number>9</number>
</property>
<item row="1" column="1">
<widget class="QComboBox" name="storage_pool">
<property name="currentText">
<string/>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Caution&lt;/span&gt;: changing these settings can compromise your system or make the qube unable to boot. Use only if you know what you are doing.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Storage pool</string>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="1" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>name</tabstop>
<tabstop>label</tabstop>
<tabstop>launch_settings</tabstop>
<tabstop>src_vm</tabstop>
<tabstop>tabWidget</tabstop>
<tabstop>storage_pool</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CloneVMDlg</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>257</x>
<y>190</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CloneVMDlg</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>325</x>
<y>190</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>