Added a Clone VM tool
A simple GUI tool to enable cloning a VM to a different pool and changing the label. references QubesOS/qubes-issues#5127
This commit is contained in:
parent
0a948aa83d
commit
1faf4f46b0
189
qubesmanager/clone_vm.py
Normal file
189
qubesmanager/clone_vm.py
Normal file
@ -0,0 +1,189 @@
|
||||
#!/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
|
||||
|
||||
self.vm_list, self.vm_idx = utils.prepare_vm_choice(
|
||||
self.src_vm,
|
||||
self.app, None,
|
||||
None,
|
||||
(lambda vm: vm.klass != 'AdminVM'),
|
||||
allow_internal=False
|
||||
)
|
||||
|
||||
if src_vm and self.src_vm.findText(src_vm.name) > -1:
|
||||
self.src_vm.setCurrentIndex(self.src_vm.findText(src_vm.name))
|
||||
|
||||
self.label_list, self.label_idx = utils.prepare_label_choice(
|
||||
self.label,
|
||||
self.app, None,
|
||||
None,
|
||||
allow_default=False
|
||||
)
|
||||
|
||||
self.update_label()
|
||||
|
||||
self.pool_list, self.pool_idx = utils.prepare_choice(
|
||||
widget=self.storage_pool,
|
||||
holder=None,
|
||||
propname=None,
|
||||
choice=self.app.pools.values(),
|
||||
default=self.app.default_pool,
|
||||
allow_default=True,
|
||||
allow_none=False
|
||||
)
|
||||
|
||||
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_list[self.label.currentIndex()]
|
||||
|
||||
if self.pool_list[self.storage_pool.currentIndex()] is not \
|
||||
qubesadmin.DEFAULT:
|
||||
pool = self.pool_list[self.storage_pool.currentIndex()]
|
||||
else:
|
||||
pool = None
|
||||
|
||||
src_vm = self.vm_list[self.src_vm.currentIndex()]
|
||||
|
||||
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.vm_list[self.src_vm.currentIndex()].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))
|
||||
|
||||
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_()
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
1
setup.py
1
setup.py
@ -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
220
ui/clonevmdlg.ui
Normal 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><html><head/><body><p><span style=" font-weight:600;">Caution</span>: changing these settings can compromise your system or make the qube unable to boot. Use only if you know what you are doing.</p></body></html></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>
|
Loading…
Reference in New Issue
Block a user