2
# Copyright (C) 2009 Red Hat, Inc.
3
# Copyright (C) 2009 Cole Robinson <crobinso@redhat.com>
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
28
from virtManager.error import vmmErrorDialog
29
from virtManager.asyncjob import vmmAsyncJob
30
from virtManager.createmeter import vmmCreateMeter
31
from virtManager.storagebrowse import vmmStorageBrowser
32
from virtManager import util
35
from virtinst import CloneManager
36
from virtinst.CloneManager import CloneDesign
37
from virtinst import VirtualNetworkInterface
39
STORAGE_COMBO_CLONE = 0
40
STORAGE_COMBO_SHARE = 1
42
STORAGE_COMBO_DETAILS = 3
44
STORAGE_INFO_ORIG_PATH = 0
45
STORAGE_INFO_NEW_PATH = 1
46
STORAGE_INFO_TARGET = 2
48
STORAGE_INFO_DEVTYPE = 4
49
STORAGE_INFO_DO_CLONE = 5
50
STORAGE_INFO_CAN_CLONE = 6
51
STORAGE_INFO_CAN_SHARE = 7
52
STORAGE_INFO_DO_DEFAULT = 8
53
STORAGE_INFO_DEFINFO = 9
54
STORAGE_INFO_FAILINFO = 10
55
STORAGE_INFO_COMBO = 11
57
NETWORK_INFO_LABEL = 0
58
NETWORK_INFO_ORIG_MAC = 1
59
NETWORK_INFO_NEW_MAC = 2
61
# XXX: Some method to check all storage size
62
# XXX: What to do for cleanup if clone fails?
63
# XXX: Disable mouse scroll for combo boxes
65
class vmmCloneVM(gobject.GObject):
67
"action-show-help": (gobject.SIGNAL_RUN_FIRST,
68
gobject.TYPE_NONE, [str]),
71
def __init__(self, config, orig_vm):
72
self.__gobject_init__()
74
self.orig_vm = orig_vm
76
self.window = gtk.glade.XML(self.config.get_glade_dir() + \
78
"vmm-clone", domain="virt-manager")
79
self.topwin = self.window.get_widget("vmm-clone")
81
self.change_mac_window = gtk.glade.XML(self.config.get_glade_dir() + \
84
domain="virt-manager")
85
self.change_mac = self.change_mac_window.get_widget("vmm-change-mac")
86
self.change_mac_window.signal_autoconnect({
87
"on_vmm_change_mac_delete_event": self.change_mac_close,
88
"on_change_mac_cancel_clicked" : self.change_mac_close,
89
"on_change_mac_ok_clicked" : self.change_mac_finish,
92
self.change_storage_window = gtk.glade.XML(self.config.get_glade_dir()\
95
domain="virt-manager")
96
self.change_storage = self.change_storage_window.get_widget("vmm-change-storage")
97
self.change_storage_window.signal_autoconnect({
98
"on_vmm_change_storage_delete_event": self.change_storage_close,
99
"on_change_storage_cancel_clicked" : self.change_storage_close,
100
"on_change_storage_ok_clicked" : self.change_storage_finish,
101
"on_change_storage_doclone_toggled" : self.change_storage_doclone_toggled,
103
"on_change_storage_browse_clicked" : self.change_storage_browse,
106
self.err = vmmErrorDialog(self.topwin,
107
0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
108
_("Unexpected Error"),
109
_("An unexpected error occurred"))
112
self.conn = self.orig_vm.connection
113
self.clone_design = None
115
self.storage_list = {}
116
self.target_list = []
121
self.storagemenu = None
123
self.storage_browser = None
125
self.window.signal_autoconnect({
126
"on_clone_delete_event" : self.close,
127
"on_clone_cancel_clicked" : self.close,
128
"on_clone_ok_clicked" : self.finish,
129
"on_clone_help_clicked" : self.show_help,
132
self.set_initial_state()
137
self.topwin.present()
139
def close(self, ignore1=None, ignore2=None):
141
self.change_mac_close()
142
self.change_storage_close()
145
def change_mac_close(self, ignore1=None, ignore2=None):
146
self.change_mac.hide()
149
def change_storage_close(self, ignore1=None, ignore2=None):
150
self.change_storage.hide()
156
def set_initial_state(self):
158
blue = gtk.gdk.color_parse("#0072A8")
159
self.window.get_widget("clone-header").modify_bg(gtk.STATE_NORMAL,
162
box = self.window.get_widget("clone-vm-icon-box")
163
image = gtk.image_new_from_icon_name("vm_clone_wizard",
164
gtk.ICON_SIZE_DIALOG)
166
box.pack_end(image, False)
169
def reset_state(self):
170
# Populate default clone values
171
self.setup_clone_info()
173
cd = self.clone_design
174
self.window.get_widget("clone-orig-name").set_text(cd.original_guest)
175
self.window.get_widget("clone-new-name").set_text(cd.clone_name)
177
# We need to determine which disks fail (and why).
178
self.storage_list, self.target_list = self.check_all_storage()
180
self.populate_storage_lists()
181
self.populate_network_list()
185
def setup_clone_info(self):
186
self.clone_design = self.build_new_clone_design()
188
def build_new_clone_design(self, new_name=None):
189
cd = CloneDesign(self.conn.vmm)
190
cd.original_guest = self.orig_vm.get_name()
192
new_name = virtinst.CloneManager.generate_clone_name(cd)
193
cd.clone_name = new_name
195
# Erase any clone_policy from the original design, so that we
196
# get the entire device list.
201
def populate_network_list(self):
202
net_box = self.window.get_widget("clone-network-box")
203
for c in net_box.get_children():
209
def build_net_row(labelstr, origmac, newmac):
211
label = gtk.Label(labelstr + " (%s)" % origmac)
212
label.set_alignment(0, .5)
213
button = gtk.Button(_("Details..."))
214
button.connect("clicked", self.net_change_mac, origmac)
218
hbox.pack_start(label)
219
hbox.pack_end(button, False, False)
221
net_box.pack_start(hbox, False, False)
224
net_row.insert(NETWORK_INFO_LABEL, labelstr)
225
net_row.insert(NETWORK_INFO_ORIG_MAC, origmac)
226
net_row.insert(NETWORK_INFO_NEW_MAC, newmac)
227
self.net_list[origmac] = net_row
228
self.mac_list.append(origmac)
230
for net in self.orig_vm.get_network_devices():
236
obj = VirtualNetworkInterface(conn=self.conn.vmm,
237
type=VirtualNetworkInterface.TYPE_USER)
238
obj.setup(self.conn.vmm)
242
# [ interface type, device name, origmac, newmac, label ]
243
if net_type == VirtualNetworkInterface.TYPE_USER:
244
label = _("Usermode")
246
elif net_type == VirtualNetworkInterface.TYPE_VIRTUAL:
247
net = self.orig_vm.get_connection().get_net_by_name(net_dev)
252
use_nat, host_dev = net.get_ipv4_forward()
254
desc = _("Isolated network")
256
desc = _("NAT to %s") % host_dev
262
label = (_("Virtual Network") +
263
(net_dev and " %s" % net_dev or ""))
266
# 'bridge' or anything else
267
label = (net_type.capitalize() +
268
net_dev and " %s" % net_dev or "")
270
build_net_row(label, mac, newmac)
272
no_net = bool(len(self.net_list.keys()) == 0)
273
self.window.get_widget("clone-network-box").set_property("visible",
275
self.window.get_widget("clone-no-net").set_property("visible", no_net)
277
def check_all_storage(self):
279
Determine which storage is cloneable, and which isn't
281
diskinfos = self.orig_vm.get_disk_devices()
282
cd = self.clone_design
286
# We need to determine which disks fail (and why).
287
all_targets = map(lambda d: d[1], diskinfos)
289
for disk in diskinfos:
290
force_target = disk[1]
302
storage_row.insert(STORAGE_INFO_ORIG_PATH, path)
303
storage_row.insert(STORAGE_INFO_NEW_PATH, clone_path)
304
storage_row.insert(STORAGE_INFO_TARGET, force_target)
305
storage_row.insert(STORAGE_INFO_SIZE, size)
306
storage_row.insert(STORAGE_INFO_DEVTYPE, devtype)
307
storage_row.insert(STORAGE_INFO_DO_CLONE, False)
308
storage_row.insert(STORAGE_INFO_CAN_CLONE, False)
309
storage_row.insert(STORAGE_INFO_CAN_SHARE, False)
310
storage_row.insert(STORAGE_INFO_DO_DEFAULT, False)
311
storage_row.insert(STORAGE_INFO_DEFINFO, definfo)
312
storage_row.insert(STORAGE_INFO_FAILINFO, failinfo)
313
storage_row.insert(STORAGE_INFO_COMBO, None)
315
skip_targets = all_targets[:]
316
skip_targets.remove(force_target)
318
vol = self.conn.get_vol_by_path(path)
319
default, definfo = do_we_default(self.conn, vol, path, ro, shared,
322
def storage_add(failinfo=None):
323
storage_row[STORAGE_INFO_DEFINFO] = definfo
324
storage_row[STORAGE_INFO_DO_DEFAULT] = default
325
storage_row[STORAGE_INFO_CAN_SHARE] = bool(definfo)
327
storage_row[STORAGE_INFO_FAILINFO] = failinfo
328
storage_row[STORAGE_INFO_DO_CLONE] = False
330
storage_list[force_target] = storage_row
332
# If origdisk is empty, deliberately make it fail
334
storage_add(_("Nothing to clone."))
338
cd.skip_target = skip_targets
341
logging.exception("Disk target '%s' caused clone error" %
346
can_clone, cloneinfo = can_we_clone(self.conn, vol, path)
348
storage_add(cloneinfo)
352
# Generate disk path, make sure that works
354
clone_path = CloneManager.generate_clone_disk_path(path, cd)
356
logging.debug("Original path: %s\nGenerated clone path: %s" %
359
cd.clone_devices = clone_path
360
size = cd.original_virtual_disks[0].size
362
logging.exception("Error setting generated path '%s'" %
366
storage_row[STORAGE_INFO_CAN_CLONE] = True
367
storage_row[STORAGE_INFO_NEW_PATH] = clone_path
368
storage_row[STORAGE_INFO_SIZE] = self.pretty_storage(size)
371
return storage_list, all_targets
373
def build_storage_entry(self, disk, storage_box):
374
origpath = disk[STORAGE_INFO_ORIG_PATH]
375
devtype = disk[STORAGE_INFO_DEVTYPE]
376
size = disk[STORAGE_INFO_SIZE]
377
can_clone = disk[STORAGE_INFO_CAN_CLONE]
378
can_share = disk[STORAGE_INFO_CAN_SHARE]
379
is_default = disk[STORAGE_INFO_DO_DEFAULT]
380
definfo = disk[STORAGE_INFO_DEFINFO]
381
failinfo = disk[STORAGE_INFO_FAILINFO]
382
target = disk[STORAGE_INFO_TARGET]
384
orig_name = self.orig_vm.get_name()
386
disk_label = os.path.basename(origpath)
389
info_label = gtk.Label()
390
info_label.set_alignment(0, .5)
391
info_label.set_markup("<span size='small'>%s</span>" % failinfo)
393
disk_label += (definfo and " (%s)" % definfo or "")
397
if devtype == virtinst.VirtualDisk.DEVICE_FLOPPY:
398
iconname = "media-floppy"
399
elif devtype == virtinst.VirtualDisk.DEVICE_CDROM:
400
iconname = "media-optical"
402
iconname = "drive-harddisk"
403
icon.set_from_icon_name(iconname, gtk.ICON_SIZE_MENU)
404
disk_name_label = gtk.Label(disk_label)
405
disk_name_label.set_alignment(0, .5)
406
disk_name_box = gtk.HBox(spacing=9)
407
disk_name_box.pack_start(icon, False)
408
disk_name_box.pack_start(disk_name_label, True)
410
def sep_func(model, it, combo):
413
# [String, sensitive, is sep]
414
model = gtk.ListStore(str, bool, bool)
415
option_combo = gtk.ComboBox(model)
416
text = gtk.CellRendererText()
417
option_combo.pack_start(text)
418
option_combo.add_attribute(text, "text", 0)
419
option_combo.add_attribute(text, "sensitive", 1)
420
option_combo.set_row_separator_func(sep_func, option_combo)
421
option_combo.connect("changed", self.storage_combo_changed, target)
423
vbox = gtk.VBox(spacing=1)
424
if can_clone or can_share:
425
model.insert(STORAGE_COMBO_CLONE,
426
[(_("Clone this disk") +
427
(size and " (%s)" % size or "")),
429
model.insert(STORAGE_COMBO_SHARE,
430
[_("Share disk with %s") % orig_name, can_share,
432
model.insert(STORAGE_COMBO_SEP, ["", False, True])
433
model.insert(STORAGE_COMBO_DETAILS,
434
[_("Details..."), True, False])
436
if can_clone and is_default:
437
option_combo.set_active(STORAGE_COMBO_CLONE)
439
option_combo.set_active(STORAGE_COMBO_SHARE)
441
model.insert(STORAGE_COMBO_CLONE,
442
[_("Storage cannot be shared or cloned."),
444
option_combo.set_active(STORAGE_COMBO_CLONE)
446
vbox.pack_start(disk_name_box, False, False)
447
vbox.pack_start(option_combo, False, False)
449
vbox.pack_start(info_label, False, False)
450
storage_box.pack_start(vbox, False, False)
452
disk[STORAGE_INFO_COMBO] = option_combo
454
def populate_storage_lists(self):
455
storage_box = self.window.get_widget("clone-storage-box")
456
for c in storage_box.get_children():
457
storage_box.remove(c)
459
for target in self.target_list:
460
disk = self.storage_list[target]
461
self.build_storage_entry(disk, storage_box)
463
num_c = min(len(self.target_list), 3)
465
scroll = self.window.get_widget("clone-storage-scroll")
466
scroll.set_size_request(-1, 80*num_c)
467
storage_box.show_all()
469
no_storage = not bool(len(self.target_list))
470
self.window.get_widget("clone-storage-box").set_property("visible",not no_storage)
471
self.window.get_widget("clone-no-storage-pass").set_property("visible", no_storage)
475
for target in self.target_list:
476
do_clone = self.storage_list[target][STORAGE_INFO_DO_CLONE]
477
new_path = self.storage_list[target][STORAGE_INFO_NEW_PATH]
480
new_disks.append(new_path)
482
skip_targets.append(target)
484
self.clone_design.skip_target = skip_targets
485
self.clone_design.clone_devices = new_disks
487
# If any storage cannot be cloned or shared, don't allow cloning
490
for row in self.storage_list.values():
491
can_clone = row[STORAGE_INFO_CAN_CLONE]
492
can_share = row[STORAGE_INFO_CAN_SHARE]
493
if not (can_clone or can_share):
495
tooltip = _("One or more disks cannot be cloned or shared.")
498
ok_button = self.window.get_widget("clone-ok")
499
ok_button.set_sensitive(clone)
500
util.tooltip_wrapper(ok_button, tooltip)
502
def net_show_popup(self, widget, event):
503
if event.button != 3:
506
self.netmenu.popup(None, None, None, 0, event.time)
508
def net_change_mac(self, ignore, origmac):
509
row = self.net_list[origmac]
510
orig_mac = row[NETWORK_INFO_ORIG_MAC]
511
new_mac = row[NETWORK_INFO_NEW_MAC]
512
typ = row[NETWORK_INFO_LABEL]
514
self.change_mac_window.get_widget("change-mac-orig").set_text(orig_mac)
515
self.change_mac_window.get_widget("change-mac-type").set_text(typ)
516
self.change_mac_window.get_widget("change-mac-new").set_text(new_mac)
518
self.change_mac.show_all()
520
def storage_show_popup(self, widget, event):
521
if event.button != 3:
524
self.storagemenu.popup(None, None, None, 0, event.time)
526
def storage_combo_changed(self, src, target):
527
idx = src.get_active()
528
row = self.storage_list[target]
530
if idx == STORAGE_COMBO_CLONE:
531
row[STORAGE_INFO_DO_CLONE] = True
533
elif idx == STORAGE_COMBO_SHARE:
534
row[STORAGE_INFO_DO_CLONE] = False
536
elif idx != STORAGE_COMBO_DETAILS:
539
do_clone = row[STORAGE_INFO_DO_CLONE]
541
src.set_active(STORAGE_COMBO_CLONE)
543
src.set_active(STORAGE_COMBO_SHARE)
546
row = self.storage_change_path(row)
548
def change_storage_doclone_toggled(self, src):
549
do_clone = src.get_active()
551
cs = self.change_storage_window
552
cs.get_widget("change-storage-new").set_sensitive(do_clone)
553
cs.get_widget("change-storage-browse").set_sensitive(do_clone)
555
def storage_change_path(self, row):
556
orig = row[STORAGE_INFO_ORIG_PATH]
557
new = row[STORAGE_INFO_NEW_PATH]
558
tgt = row[STORAGE_INFO_TARGET]
559
size = row[STORAGE_INFO_SIZE]
560
can_clone = row[STORAGE_INFO_CAN_CLONE]
561
can_share = row[STORAGE_INFO_CAN_SHARE]
562
do_clone = row[STORAGE_INFO_DO_CLONE]
564
cs = self.change_storage_window
565
cs.get_widget("change-storage-doclone").set_active(True)
566
cs.get_widget("change-storage-doclone").toggled()
567
cs.get_widget("change-storage-orig").set_text(orig)
568
cs.get_widget("change-storage-target").set_text(tgt)
569
cs.get_widget("change-storage-size").set_text(size or "-")
570
cs.get_widget("change-storage-doclone").set_active(do_clone)
573
cs.get_widget("change-storage-new").set_text(new or "")
575
cs.get_widget("change-storage-new").set_text("")
576
cs.get_widget("change-storage-doclone").set_sensitive(can_clone and
579
cs.get_widget("vmm-change-storage").show_all()
581
def set_orig_vm(self, new_orig):
582
self.orig_vm = new_orig
583
self.conn = self.orig_vm.connection
585
def change_mac_finish(self, ignore):
586
orig = self.change_mac_window.get_widget("change-mac-orig").get_text()
587
new = self.change_mac_window.get_widget("change-mac-new").get_text()
588
row = self.net_list[orig]
591
VirtualNetworkInterface(conn=self.conn.vmm,
592
type=VirtualNetworkInterface.TYPE_USER,
594
row[NETWORK_INFO_NEW_MAC] = new
596
self.err.show_err(_("Error changing MAC address: %s") % str(e),
597
"".join(traceback.format_exc()))
600
self.change_mac_close()
602
def change_storage_finish(self, ignore):
603
cs = self.change_storage_window
604
target = cs.get_widget("change-storage-target").get_text()
605
row = self.storage_list[target]
607
# Sync 'do clone' checkbox, and main dialog combo
608
combo = row[STORAGE_INFO_COMBO]
609
if cs.get_widget("change-storage-doclone").get_active():
610
combo.set_active(STORAGE_COMBO_CLONE)
612
combo.set_active(STORAGE_COMBO_SHARE)
614
do_clone = row[STORAGE_INFO_DO_CLONE]
616
self.change_storage_close()
619
new_path = cs.get_widget("change-storage-new").get_text()
621
if virtinst.VirtualDisk.path_exists(self.clone_design.original_conn,
623
res = self.err.yes_no(_("Cloning will overwrite the existing "
625
_("Using an existing image will overwrite "
626
"the path during the clone process. Are "
627
"you sure you want to use this path?"))
632
self.clone_design.clone_devices = new_path
633
self.populate_storage_lists()
634
row[STORAGE_INFO_NEW_PATH] = new_path
636
self.err.show_err(_("Error changing storage path: %s") % str(e),
637
"".join(traceback.format_exc()))
640
self.change_storage_close()
642
def pretty_storage(self, size):
645
return "%.1f GB" % float(size)
649
name = self.window.get_widget("clone-new-name").get_text()
651
# Make another clone_design
652
cd = self.build_new_clone_design(name)
655
for mac in self.mac_list:
656
row = self.net_list[mac]
657
new_mac = row[NETWORK_INFO_NEW_MAC]
658
cd.clone_mac = new_mac
663
for target in self.target_list:
664
path = self.storage_list[target][STORAGE_INFO_ORIG_PATH]
665
new_path = self.storage_list[target][STORAGE_INFO_NEW_PATH]
666
do_clone = self.storage_list[target][STORAGE_INFO_DO_CLONE]
667
do_default = self.storage_list[target][STORAGE_INFO_DO_DEFAULT]
670
new_paths.append(new_path)
672
skip_targets.append(target)
673
if not path or path == '-':
679
warn_str += "%s: %s\n" % (target, path)
681
cd.skip_target = skip_targets
683
cd.clone_devices = new_paths
686
res = self.err.ok_cancel(
687
_("Skipping disks may cause data to be overwritten."),
688
_("The following disk devices will not be cloned:\n\n%s\n"
689
"Running the new guest could overwrite data in these "
698
self.clone_design = cd
701
def finish(self, src):
705
if not self.validate():
708
self.err.show_err(_("Uncaught error validating input: %s") % str(e),
709
"".join(traceback.format_exc()))
712
self.topwin.set_sensitive(False)
713
self.topwin.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
715
title = (_("Creating virtual machine clone '%s'") %
716
self.clone_design.clone_name)
718
if self.clone_design.clone_devices:
719
text = title + _(" and selected storage (this may take a while)")
721
progWin = vmmAsyncJob(self.config, self._async_clone, [],
722
title=title, text=text)
724
error, details = progWin.get_error()
726
self.topwin.set_sensitive(True)
727
self.topwin.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_ARROW))
729
if error is not None:
730
self.err.show_err(error, details)
733
self.conn.tick(noStatsUpdate=True)
735
def _async_clone(self, asyncjob):
741
# Open a seperate connection to install on since this is async
742
logging.debug("Threading off connection to clone VM.")
743
newconn = util.dup_conn(self.config, self.conn)
744
meter = vmmCreateMeter(asyncjob)
746
self.clone_design.orig_connection = newconn
747
for d in self.clone_design.clone_virtual_disks:
750
self.clone_design.setup()
751
CloneManager.start_duplicate(self.clone_design, meter)
753
error = (_("Error creating virtual machine clone '%s': %s") %
754
(self.clone_design.clone_name, str(e)))
755
details = "".join(traceback.format_exc())
758
asyncjob.set_error(error, details)
761
def change_storage_browse(self, ignore):
763
cs = self.change_storage_window
764
def callback(self, txt):
765
cs.get_widget("change-storage-new").set_text(txt)
767
if self.storage_browser == None:
768
self.storage_browser = vmmStorageBrowser(self.config, self.conn)
769
self.storage_browser.connect("storage-browse-finish", callback)
771
self.storage_browser.show(self.conn)
773
def show_help(self, ignore1=None):
777
gobject.type_register(vmmCloneVM)
779
def can_we_clone(conn, vol, path):
780
"""Is the passed path even clone-able"""
784
if not path or path == "-":
785
msg = _("No storage to clone.")
789
if not virtinst.Storage.is_create_vol_from_supported(conn):
790
if conn.is_remote() or not os.access(path, os.R_OK):
791
msg = _("Connection does not support managed storage cloning.")
794
msg = _("Cannot clone unmanaged remote storage.")
795
elif not os.access(path, os.R_OK):
796
msg = _("No write access to parent directory.")
797
elif not os.path.exists(path):
798
msg = _("Path does not exist.")
805
def do_we_default(conn, vol, path, ro, shared, devtype):
806
""" Returns (do we clone by default?, info string if not)"""
809
def append_str(str1, str2, delim=", "):
817
if (devtype == virtinst.VirtualDisk.DEVICE_CDROM or
818
devtype == virtinst.VirtualDisk.DEVICE_FLOPPY):
819
info = append_str(info, _("Removable"))
822
info = append_str(info, _("Read Only"))
823
elif not vol and not os.access(path, os.W_OK):
824
info = append_str(info, _("No write access"))
827
info = append_str(info, _("Shareable"))
829
return (not info, info)