From f23a27639f61d8eeb0268d03ccaefecc1be78637 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 3 Sep 2020 12:03:38 -0400 Subject: [PATCH] cloner: Big API rework * Centralize lots of disk building * Open code virt-clone specific behavior at the source * Drop a lot or properties * Move most testing to test_cli.py * Generally a ton of cleanup virt-manager clone wizard has not been converted yet so is totally broken after this commit Signed-off-by: Cole Robinson --- .../cli/{ => clone}/clone-disk-managed.xml | 0 .../cli/{ => clone}/clone-disk-noexist.xml | 0 tests/data/cli/{ => clone}/clone-disk.xml | 0 tests/data/cli/{ => clone}/clone-empty.xml | 2 +- .../data/cli/{ => clone}/clone-nvram-auto.xml | 0 .../cli/{ => clone}/clone-nvram-missing.xml | 0 .../cli/{ => clone}/clone-nvram-newpool.xml | 0 .../compare/virt-clone-clone-nvram-path.xml} | 6 +- tests/data/cli/compare/virt-clone-empty.xml | 2 +- tests/data/cli/compare/virt-clone-replace.xml | 83 ++ tests/data/clone/channel-source-in.xml | 35 - tests/data/clone/channel-source-out.xml | 35 - tests/data/clone/cross-pool-disks-out.xml | 22 - tests/data/clone/cross-pool-in.xml | 39 - tests/data/clone/cross-pool-out.xml | 39 - tests/data/clone/empty-disks-in.xml | 46 - tests/data/clone/empty-disks-out.xml | 46 - tests/data/clone/force-in.xml | 46 - tests/data/clone/force-out.xml | 46 - tests/data/clone/fullpool-in.xml | 29 - tests/data/clone/general-cfg-in.xml | 43 - tests/data/clone/general-cfg-out.xml | 40 - tests/data/clone/graphics-password-in.xml | 23 - tests/data/clone/graphics-password-out.xml | 23 - tests/data/clone/managed-storage-in.xml | 40 - tests/data/clone/managed-storage-out.xml | 40 - tests/data/clone/noclone-storage-in.xml | 43 - tests/data/clone/noclone-storage-out.xml | 43 - tests/data/clone/nostorage-in.xml | 18 - tests/data/clone/nostorage-out.xml | 17 - tests/data/clone/nvram-auto-in.xml | 23 - tests/data/clone/nvram-auto-out.xml | 23 - tests/data/clone/nvram-missing-in.xml | 23 - tests/data/clone/nvram-newpool-in.xml | 23 - tests/data/clone/nvram-newpool-out.xml | 23 - tests/data/clone/readonly-disks-in.xml | 54 -- tests/data/clone/readonly-disks-out.xml | 54 -- tests/data/clone/skip-in.xml | 46 - tests/data/clone/skip-out.xml | 46 - tests/test_cli.py | 33 +- tests/test_cloner.py | 217 +---- tests/test_xmlconfig.py | 12 + virtinst/cloner.py | 846 ++++++++---------- virtinst/diskbackend.py | 2 +- virtinst/virtclone.py | 162 ++-- 45 files changed, 580 insertions(+), 1813 deletions(-) rename tests/data/cli/{ => clone}/clone-disk-managed.xml (100%) rename tests/data/cli/{ => clone}/clone-disk-noexist.xml (100%) rename tests/data/cli/{ => clone}/clone-disk.xml (100%) rename tests/data/cli/{ => clone}/clone-empty.xml (93%) rename tests/data/cli/{ => clone}/clone-nvram-auto.xml (100%) rename tests/data/cli/{ => clone}/clone-nvram-missing.xml (100%) rename tests/data/cli/{ => clone}/clone-nvram-newpool.xml (100%) rename tests/data/{clone/nvram-missing-out.xml => cli/compare/virt-clone-clone-nvram-path.xml} (80%) create mode 100644 tests/data/cli/compare/virt-clone-replace.xml delete mode 100644 tests/data/clone/channel-source-in.xml delete mode 100644 tests/data/clone/channel-source-out.xml delete mode 100644 tests/data/clone/cross-pool-disks-out.xml delete mode 100644 tests/data/clone/cross-pool-in.xml delete mode 100644 tests/data/clone/cross-pool-out.xml delete mode 100644 tests/data/clone/empty-disks-in.xml delete mode 100644 tests/data/clone/empty-disks-out.xml delete mode 100644 tests/data/clone/force-in.xml delete mode 100644 tests/data/clone/force-out.xml delete mode 100644 tests/data/clone/fullpool-in.xml delete mode 100644 tests/data/clone/general-cfg-in.xml delete mode 100644 tests/data/clone/general-cfg-out.xml delete mode 100644 tests/data/clone/graphics-password-in.xml delete mode 100644 tests/data/clone/graphics-password-out.xml delete mode 100644 tests/data/clone/managed-storage-in.xml delete mode 100644 tests/data/clone/managed-storage-out.xml delete mode 100644 tests/data/clone/noclone-storage-in.xml delete mode 100644 tests/data/clone/noclone-storage-out.xml delete mode 100644 tests/data/clone/nostorage-in.xml delete mode 100644 tests/data/clone/nostorage-out.xml delete mode 100644 tests/data/clone/nvram-auto-in.xml delete mode 100644 tests/data/clone/nvram-auto-out.xml delete mode 100644 tests/data/clone/nvram-missing-in.xml delete mode 100644 tests/data/clone/nvram-newpool-in.xml delete mode 100644 tests/data/clone/nvram-newpool-out.xml delete mode 100644 tests/data/clone/readonly-disks-in.xml delete mode 100644 tests/data/clone/readonly-disks-out.xml delete mode 100644 tests/data/clone/skip-in.xml delete mode 100644 tests/data/clone/skip-out.xml diff --git a/tests/data/cli/clone-disk-managed.xml b/tests/data/cli/clone/clone-disk-managed.xml similarity index 100% rename from tests/data/cli/clone-disk-managed.xml rename to tests/data/cli/clone/clone-disk-managed.xml diff --git a/tests/data/cli/clone-disk-noexist.xml b/tests/data/cli/clone/clone-disk-noexist.xml similarity index 100% rename from tests/data/cli/clone-disk-noexist.xml rename to tests/data/cli/clone/clone-disk-noexist.xml diff --git a/tests/data/cli/clone-disk.xml b/tests/data/cli/clone/clone-disk.xml similarity index 100% rename from tests/data/cli/clone-disk.xml rename to tests/data/cli/clone/clone-disk.xml diff --git a/tests/data/cli/clone-empty.xml b/tests/data/cli/clone/clone-empty.xml similarity index 93% rename from tests/data/cli/clone-empty.xml rename to tests/data/cli/clone/clone-empty.xml index 08d5699e..538d9f92 100644 --- a/tests/data/cli/clone-empty.xml +++ b/tests/data/cli/clone/clone-empty.xml @@ -1,5 +1,5 @@ - clone-empty + empty-clone5 4a64cc71-7272-2fd0-2323-3050941ea3c3 8388608 2097152 diff --git a/tests/data/cli/clone-nvram-auto.xml b/tests/data/cli/clone/clone-nvram-auto.xml similarity index 100% rename from tests/data/cli/clone-nvram-auto.xml rename to tests/data/cli/clone/clone-nvram-auto.xml diff --git a/tests/data/cli/clone-nvram-missing.xml b/tests/data/cli/clone/clone-nvram-missing.xml similarity index 100% rename from tests/data/cli/clone-nvram-missing.xml rename to tests/data/cli/clone/clone-nvram-missing.xml diff --git a/tests/data/cli/clone-nvram-newpool.xml b/tests/data/cli/clone/clone-nvram-newpool.xml similarity index 100% rename from tests/data/cli/clone-nvram-newpool.xml rename to tests/data/cli/clone/clone-nvram-newpool.xml diff --git a/tests/data/clone/nvram-missing-out.xml b/tests/data/cli/compare/virt-clone-clone-nvram-path.xml similarity index 80% rename from tests/data/clone/nvram-missing-out.xml rename to tests/data/cli/compare/virt-clone-clone-nvram-path.xml index c59eaea8..590f4c9b 100644 --- a/tests/data/clone/nvram-missing-out.xml +++ b/tests/data/cli/compare/virt-clone-clone-nvram-path.xml @@ -1,6 +1,6 @@ - clone-new - 12345678-1234-1234-1234-123456789012 + clone-orig-clone + 00000000-1111-2222-3333-444444444444 262144 262144 1 @@ -8,7 +8,7 @@ hvm /usr/share/ovmf/ovmf-efi.fd - /nvram/clone-new_VARS.fd + /nvram/my-custom-path diff --git a/tests/data/cli/compare/virt-clone-empty.xml b/tests/data/cli/compare/virt-clone-empty.xml index cf794bcb..939d0c59 100644 --- a/tests/data/cli/compare/virt-clone-empty.xml +++ b/tests/data/cli/compare/virt-clone-empty.xml @@ -1,5 +1,5 @@ - clone-empty-clone + empty-clone6 00000000-1111-2222-3333-444444444444 8388608 2097152 diff --git a/tests/data/cli/compare/virt-clone-replace.xml b/tests/data/cli/compare/virt-clone-replace.xml new file mode 100644 index 00000000..4c94c6b6 --- /dev/null +++ b/tests/data/cli/compare/virt-clone-replace.xml @@ -0,0 +1,83 @@ + + test + 00000000-1111-2222-3333-444444444444 + 409600 + 204800 + 5 + + hvm + /usr/lib/xen/boot/hvmloader + + + + + + + + destroy + restart + destroy + + /usr/lib/xen/bin/qemu-dm + + + + +
+ + + + + +
+ + + + +
+ + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/clone/channel-source-in.xml b/tests/data/clone/channel-source-in.xml deleted file mode 100644 index f69054c4..00000000 --- a/tests/data/clone/channel-source-in.xml +++ /dev/null @@ -1,35 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/channel-source-out.xml b/tests/data/clone/channel-source-out.xml deleted file mode 100644 index 210e7a7b..00000000 --- a/tests/data/clone/channel-source-out.xml +++ /dev/null @@ -1,35 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/cross-pool-disks-out.xml b/tests/data/clone/cross-pool-disks-out.xml deleted file mode 100644 index 260f321c..00000000 --- a/tests/data/clone/cross-pool-disks-out.xml +++ /dev/null @@ -1,22 +0,0 @@ - - new1.img - 1000000 - 50000 - - - - - - - - - new2.img - 1000000 - 50000 - - - - - - - diff --git a/tests/data/clone/cross-pool-in.xml b/tests/data/clone/cross-pool-in.xml deleted file mode 100644 index 6177a6f9..00000000 --- a/tests/data/clone/cross-pool-in.xml +++ /dev/null @@ -1,39 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/cross-pool-out.xml b/tests/data/clone/cross-pool-out.xml deleted file mode 100644 index 5541adf7..00000000 --- a/tests/data/clone/cross-pool-out.xml +++ /dev/null @@ -1,39 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/empty-disks-in.xml b/tests/data/clone/empty-disks-in.xml deleted file mode 100644 index a88c2666..00000000 --- a/tests/data/clone/empty-disks-in.xml +++ /dev/null @@ -1,46 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/empty-disks-out.xml b/tests/data/clone/empty-disks-out.xml deleted file mode 100644 index 318f08d9..00000000 --- a/tests/data/clone/empty-disks-out.xml +++ /dev/null @@ -1,46 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/force-in.xml b/tests/data/clone/force-in.xml deleted file mode 100644 index 1a431e8f..00000000 --- a/tests/data/clone/force-in.xml +++ /dev/null @@ -1,46 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/force-out.xml b/tests/data/clone/force-out.xml deleted file mode 100644 index 63bf6102..00000000 --- a/tests/data/clone/force-out.xml +++ /dev/null @@ -1,46 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/fullpool-in.xml b/tests/data/clone/fullpool-in.xml deleted file mode 100644 index 9b5cc47a..00000000 --- a/tests/data/clone/fullpool-in.xml +++ /dev/null @@ -1,29 +0,0 @@ - - test-full-clone - 204800 - 409600 - abcd5678-aaaa-1234-1234-12345678FFFF - - hvm - /usr/lib/xen/boot/hvmloader - - - - - - - destroy - restart - destroy - 5 - - /usr/lib/xen/bin/qemu-dm - - - - - - - - - diff --git a/tests/data/clone/general-cfg-in.xml b/tests/data/clone/general-cfg-in.xml deleted file mode 100644 index b859bdf2..00000000 --- a/tests/data/clone/general-cfg-in.xml +++ /dev/null @@ -1,43 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - footitle - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/general-cfg-out.xml b/tests/data/clone/general-cfg-out.xml deleted file mode 100644 index 961d7aba..00000000 --- a/tests/data/clone/general-cfg-out.xml +++ /dev/null @@ -1,40 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/graphics-password-in.xml b/tests/data/clone/graphics-password-in.xml deleted file mode 100644 index 13b14dd6..00000000 --- a/tests/data/clone/graphics-password-in.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - diff --git a/tests/data/clone/graphics-password-out.xml b/tests/data/clone/graphics-password-out.xml deleted file mode 100644 index e730efef..00000000 --- a/tests/data/clone/graphics-password-out.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - diff --git a/tests/data/clone/managed-storage-in.xml b/tests/data/clone/managed-storage-in.xml deleted file mode 100644 index 8cac51ee..00000000 --- a/tests/data/clone/managed-storage-in.xml +++ /dev/null @@ -1,40 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/managed-storage-out.xml b/tests/data/clone/managed-storage-out.xml deleted file mode 100644 index 540f8d7a..00000000 --- a/tests/data/clone/managed-storage-out.xml +++ /dev/null @@ -1,40 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/noclone-storage-in.xml b/tests/data/clone/noclone-storage-in.xml deleted file mode 100644 index 26303835..00000000 --- a/tests/data/clone/noclone-storage-in.xml +++ /dev/null @@ -1,43 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/noclone-storage-out.xml b/tests/data/clone/noclone-storage-out.xml deleted file mode 100644 index a76efba1..00000000 --- a/tests/data/clone/noclone-storage-out.xml +++ /dev/null @@ -1,43 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/nostorage-in.xml b/tests/data/clone/nostorage-in.xml deleted file mode 100644 index 72b97742..00000000 --- a/tests/data/clone/nostorage-in.xml +++ /dev/null @@ -1,18 +0,0 @@ - - clone-orig - 19618dc6-7895-956d-6056-8ebcd8061234 - 8388608 - 2097152 - 2 - - hvm - - - - destroy - restart - destroy - - - - diff --git a/tests/data/clone/nostorage-out.xml b/tests/data/clone/nostorage-out.xml deleted file mode 100644 index 990a7712..00000000 --- a/tests/data/clone/nostorage-out.xml +++ /dev/null @@ -1,17 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 8388608 - 2097152 - 2 - - hvm - - - - destroy - restart - destroy - - - diff --git a/tests/data/clone/nvram-auto-in.xml b/tests/data/clone/nvram-auto-in.xml deleted file mode 100644 index dc95f6ac..00000000 --- a/tests/data/clone/nvram-auto-in.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - /usr/share/ovmf/ovmf-efi.fd - /nvram/clone-orig_VARS.fd - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - diff --git a/tests/data/clone/nvram-auto-out.xml b/tests/data/clone/nvram-auto-out.xml deleted file mode 100644 index c59eaea8..00000000 --- a/tests/data/clone/nvram-auto-out.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - /usr/share/ovmf/ovmf-efi.fd - /nvram/clone-new_VARS.fd - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - diff --git a/tests/data/clone/nvram-missing-in.xml b/tests/data/clone/nvram-missing-in.xml deleted file mode 100644 index fcbce533..00000000 --- a/tests/data/clone/nvram-missing-in.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - /usr/share/ovmf/ovmf-efi.fd - /nvram/clone-orig-missing_VARS.fd - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - diff --git a/tests/data/clone/nvram-newpool-in.xml b/tests/data/clone/nvram-newpool-in.xml deleted file mode 100644 index d27da472..00000000 --- a/tests/data/clone/nvram-newpool-in.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - /usr/share/ovmf/ovmf-efi.fd - /nvram-newpool/clone-orig-vars.fd - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - diff --git a/tests/data/clone/nvram-newpool-out.xml b/tests/data/clone/nvram-newpool-out.xml deleted file mode 100644 index 42d95460..00000000 --- a/tests/data/clone/nvram-newpool-out.xml +++ /dev/null @@ -1,23 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - /usr/share/ovmf/ovmf-efi.fd - /nvram-newpool/clone-new_VARS.fd - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - diff --git a/tests/data/clone/readonly-disks-in.xml b/tests/data/clone/readonly-disks-in.xml deleted file mode 100644 index c3962c9a..00000000 --- a/tests/data/clone/readonly-disks-in.xml +++ /dev/null @@ -1,54 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/readonly-disks-out.xml b/tests/data/clone/readonly-disks-out.xml deleted file mode 100644 index 83e1b385..00000000 --- a/tests/data/clone/readonly-disks-out.xml +++ /dev/null @@ -1,54 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/skip-in.xml b/tests/data/clone/skip-in.xml deleted file mode 100644 index c04a6d6b..00000000 --- a/tests/data/clone/skip-in.xml +++ /dev/null @@ -1,46 +0,0 @@ - - clone-orig - aaa3ae22-fed2-bfbd-ac02-3bea3bcfad82 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/clone/skip-out.xml b/tests/data/clone/skip-out.xml deleted file mode 100644 index 01ba3e6f..00000000 --- a/tests/data/clone/skip-out.xml +++ /dev/null @@ -1,46 +0,0 @@ - - clone-new - 12345678-1234-1234-1234-123456789012 - 262144 - 262144 - 1 - - hvm - - - - - - - destroy - restart - destroy - - /usr/bin/qemu-kvm - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/test_cli.py b/tests/test_cli.py index c50abf9c..a1de05e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1298,13 +1298,14 @@ c.add_compare("--add-device --network default --os-variant http://fedoraproject. # virt-clone tests # #################### -_CLONE_UNMANAGED = "--original-xml %s/clone-disk.xml" % XMLDIR -_CLONE_MANAGED = "--original-xml %s/clone-disk-managed.xml" % XMLDIR -_CLONE_NOEXIST = "--original-xml %s/clone-disk-noexist.xml" % XMLDIR -_CLONE_NVRAM = "--original-xml %s/clone-nvram-auto.xml" % XMLDIR -_CLONE_NVRAM_NEWPOOL = "--original-xml %s/clone-nvram-newpool.xml" % XMLDIR -_CLONE_NVRAM_MISSING = "--original-xml %s/clone-nvram-missing.xml" % XMLDIR -_CLONE_EMPTY = "--original-xml %s/clone-empty.xml" % XMLDIR +_CLONEXMLDIR = XMLDIR + "/clone" +_CLONE_UNMANAGED = "--original-xml %s/clone-disk.xml" % _CLONEXMLDIR +_CLONE_MANAGED = "--original-xml %s/clone-disk-managed.xml" % _CLONEXMLDIR +_CLONE_NOEXIST = "--original-xml %s/clone-disk-noexist.xml" % _CLONEXMLDIR +_CLONE_NVRAM = "--original-xml %s/clone-nvram-auto.xml" % _CLONEXMLDIR +_CLONE_NVRAM_NEWPOOL = "--original-xml %s/clone-nvram-newpool.xml" % _CLONEXMLDIR +_CLONE_NVRAM_MISSING = "--original-xml %s/clone-nvram-missing.xml" % _CLONEXMLDIR +_CLONE_EMPTY = "--original-xml %s/clone-empty.xml" % _CLONEXMLDIR vclon = App("virt-clone") c = vclon.add_category("remote", "--connect %(URI-TEST-REMOTE)s") @@ -1318,6 +1319,7 @@ c = vclon.add_category("misc", "") c.add_compare("--connect %(URI-KVM)s -o test-clone --auto-clone", "clone-auto1") c.add_compare("--connect %(URI-TEST-FULL)s -o test-clone-simple --name newvm --auto-clone", "clone-auto2") c.add_compare("--connect %(URI-KVM)s " + _CLONE_NVRAM + " --auto-clone", "clone-nvram") # hits a particular nvram code path +c.add_compare("--connect %(URI-KVM)s " + _CLONE_NVRAM + " --auto-clone --nvram /nvram/my-custom-path", "clone-nvram-path") # hits a particular nvram code path c.add_compare("--connect %(URI-KVM)s " + _CLONE_NVRAM_NEWPOOL + " --auto-clone", "nvram-newpool") # hits a particular nvram code path c.add_compare("--connect %(URI-KVM)s " + _CLONE_NVRAM_MISSING + " --auto-clone", "nvram-missing") # hits a particular nvram code path c.add_compare("--connect %(URI-KVM)s -o test-clone -n test-newclone --mac 12:34:56:1A:B2:C3 --mac 12:34:56:1A:B7:C3 --uuid 12345678-12F4-1234-1234-123456789AFA --file /dev/disk-pool/newclone1.img --file /dev/default-pool/newclone2.img --skip-copy=hdb --force-copy=sdb --file /dev/default-pool/newclone3.img", "clone-manual") @@ -1333,14 +1335,14 @@ c.add_invalid("-n clonetest " + _CLONE_UNMANAGED + " --auto-clone --mac 22:11:11 c.add_invalid("--auto-clone") # Just the auto flag c.add_invalid(_CLONE_EMPTY + " --file foo") # Didn't specify new name c.add_invalid(_CLONE_EMPTY + " --auto-clone -n test") # new name raises error -c.add_invalid("-o test --auto-clone", grep="shutoff") # VM is running, but --clone-running isn't passed +c.add_invalid("-o test --auto-clone", grep="shutoff") # VM is running c.add_invalid("--connect %(URI-TEST-FULL)s -o test-clone-simple -n newvm --file %(EXISTIMG1)s") # Should complain about overwriting existing file c.add_invalid("--connect %(URI-TEST-REMOTE)s -o test-clone-simple --auto-clone --file /dev/default-pool/testvol9.img --check all=off", grep="Clone onto existing storage volume") # hit a specific error message c.add_invalid("--connect %(URI-TEST-FULL)s -o test-clone-full --auto-clone", grep="not enough free space") # catch failure of clone path setting c = vclon.add_category("general", "-n clonetest") -c.add_valid("-o test --clone-running --auto-clone --replace") # Auto flag, no storage, --replace is redundant +c.add_valid(_CLONE_EMPTY + " --auto-clone --replace") # --replace but it doesn't matter, should be safely ignored c.add_valid(_CLONE_EMPTY + " --file %(NEWCLONEIMG1)s --file %(NEWCLONEIMG2)s") # Nodisk, but with spurious files passed c.add_valid(_CLONE_EMPTY + " --file %(NEWCLONEIMG1)s --file %(NEWCLONEIMG2)s --prompt") # Working scenario w/ prompt shouldn't ask anything c.add_valid(_CLONE_UNMANAGED + " --file %(NEWCLONEIMG1)s --file %(NEWCLONEIMG2)s") # XML File with 2 disks @@ -1350,18 +1352,17 @@ c.add_valid(_CLONE_UNMANAGED + " --file %(NEWCLONEIMG1)s --file %(NEWCLONEIMG2)s c.add_valid(_CLONE_UNMANAGED + " --file %(NEWCLONEIMG1)s --file %(NEWCLONEIMG2)s --force-copy=fda") # XML w/ disks, force copy a target with no media c.add_valid(_CLONE_MANAGED + " --file %(NEWIMG1)s") # XML w/ managed storage, specify managed path c.add_valid(_CLONE_MANAGED + " --file %(NEWIMG1)s --reflink") # XML w/ managed storage, specify managed path, use --reflink option -c.add_valid(_CLONE_NOEXIST + " --file %(EXISTIMG1)s --preserve") # XML w/ managed storage, specify managed path across pools# Libvirt test driver doesn't support cloning across pools# XML w/ non-existent storage, with --preserve -c.add_valid("--connect %(URI-TEST-FULL)s -o test-clone -n test --auto-clone --replace") # Overwriting existing running VM +c.add_valid(_CLONE_NOEXIST + " --file %(EXISTIMG1)s --preserve") # XML w/ managed storage, specify managed path across pools +c.add_compare("--connect %(URI-TEST-FULL)s -o test-clone -n test --auto-clone --replace", "replace") # Overwriting existing running VM +c.add_valid(_CLONE_MANAGED + " --auto-clone --force-copy fda") # force copy empty floppy drive c.add_invalid(_CLONE_EMPTY + " foobar") # Positional arguments error c.add_invalid("-o idontexist") # Non-existent vm name c.add_invalid("-o idontexist --auto-clone") # Non-existent vm name with auto flag, c.add_invalid(_CLONE_EMPTY + " -n test") # Colliding new name c.add_invalid(_CLONE_UNMANAGED + "") # XML file with several disks, but non specified -c.add_invalid(_CLONE_UNMANAGED + " --file virt-install --file %(EXISTIMG1)s") # XML w/ disks, overwriting existing files with no --preserve -c.add_invalid(_CLONE_UNMANAGED + " --file %(NEWCLONEIMG1)s --file %(NEWCLONEIMG2)s --force-copy=hdc") # XML w/ disks, force copy but not enough disks passed -c.add_invalid(_CLONE_MANAGED + " --file /tmp/clonevol") # XML w/ managed storage, specify unmanaged path (should fail) -c.add_invalid(_CLONE_NOEXIST + " --file %(EXISTIMG1)s") # XML w/ non-existent storage, WITHOUT --preserve -c.add_valid(_CLONE_MANAGED + " --auto-clone --force-copy fda") # force copy empty floppy drive +c.add_invalid(_CLONE_UNMANAGED + " --file virt-install", grep="overwrite the existing path 'virt-install'") # XML w/ disks, overwriting existing files with no --preserve +c.add_invalid(_CLONE_MANAGED + " --file /tmp/clonevol", grep="matching name 'default-vol'") # will attempt to clone across pools, which test driver doesn't support +c.add_invalid(_CLONE_NOEXIST + " --auto-clone", grep="'/i/really/dont/exist' does not exist.") # XML w/ non-existent storage, WITHOUT --preserve diff --git a/tests/test_cloner.py b/tests/test_cloner.py index 91e19bb5..f52e5147 100644 --- a/tests/test_cloner.py +++ b/tests/test_cloner.py @@ -3,209 +3,42 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -import unittest import os +import tempfile from tests import utils from virtinst import Cloner -from virtinst import log - -ORIG_NAME = "clone-orig" -CLONE_NAME = "clone-new" - -# Create some files to use as test images -FILE1 = "/tmp/virtinst-test1.img" -FILE2 = "/tmp/virtinst-test2.img" -P1_VOL1 = "/dev/default-pool/testvol1.img" -P1_VOL2 = "/dev/default-pool/testvol2.img" -P2_VOL1 = "/dev/cross-pool/testvol1.img" -P2_VOL2 = "/dev/cross-pool/testvol2.img" - -POOL1 = "/dev/default-pool" -POOL2 = "/dev/cross-pool" -DISKPOOL = "/dev/disk-pool" - -local_files = [FILE1, FILE2] - -CLONEXML_DIR = os.path.join(utils.DATADIR, "clone") -class TestClone(unittest.TestCase): +CLI_XMLDIR = utils.DATADIR + "/cli/clone/" - def setUp(self): - for f in local_files: - open(f, "w").write("") - def tearDown(self): - for f in local_files: - os.unlink(f) +def test_clone_unmanaged(): + """ + Test that unmanaged storage duplication via the clone wizard + actually copies data + """ + xmlpath = CLI_XMLDIR + "clone-disk.xml" + conn = utils.URIs.open_testdriver_cached() + xml = open(xmlpath).read() - def _clone(self, filebase, disks=None, force_list=None, - skip_list=None, compare=True, conn=None, - clone_disks_file=None): - """Helper for comparing clone input/output from 2 xml files""" - infile = os.path.join(CLONEXML_DIR, filebase + "-in.xml") - in_content = open(infile).read() + tmp1 = tempfile.NamedTemporaryFile() + tmp2 = tempfile.NamedTemporaryFile() + inp1 = os.path.abspath(__file__) + inp2 = xmlpath - if not conn: - conn = utils.URIs.open_testdriver_cached() - cloneobj = Cloner(conn) - cloneobj.original_xml = in_content + xml = xml.replace("/tmp/__virtinst_cli_exist1.img", inp1) + xml = xml.replace("/tmp/__virtinst_cli_exist2.img", inp2) + cloner = Cloner(conn, src_xml=xml) - force_list = force_list or [] - for force in force_list: - cloneobj.force_target = force - self.assertEqual(cloneobj.force_target, force_list) - cloneobj.force_target = force_list - self.assertEqual(cloneobj.force_target, force_list) + diskinfos = cloner.get_diskinfos_to_clone() + assert len(diskinfos) == 2 + diskinfos[0].set_clone_path(tmp1.name, True, False) + diskinfos[1].set_clone_path(tmp2.name, True, False) - skip_list = skip_list or [] - for skip in skip_list: - cloneobj.skip_target = skip - self.assertEqual(cloneobj.skip_target, skip_list) - cloneobj.skip_target = skip_list - self.assertEqual(cloneobj.skip_target, skip_list) + cloner.prepare() + cloner.start_duplicate(None) - cloneobj = self._default_clone_values(cloneobj, disks) - - if compare: - self._clone_compare(cloneobj, filebase, - clone_disks_file=clone_disks_file) - self._clone_define(filebase) - else: - cloneobj.setup_original() - cloneobj.setup_clone() - - def _default_clone_values(self, cloneobj, disks=None): - """Sets default values for the cloned VM.""" - cloneobj.clone_name = "clone-new" - - uuid = "12345678-1234-1234-1234-123456789012" - cloneobj.clone_uuid = uuid - self.assertEqual(cloneobj.clone_uuid, uuid) - - macs = ["22:23:45:67:89:00", "22:23:45:67:89:01"] - cloneobj.clone_macs = macs - self.assertEqual(cloneobj.clone_macs, macs) - - if disks is None: - disks = ["/dev/disk-pool/disk-vol1", "/tmp/clone2.img", - "/clone3", "/tmp/clone4.img", - "/tmp/clone5.img", None] - - cloneobj.clone_paths = disks - self.assertEqual(cloneobj.clone_paths, disks) - return cloneobj - - def _clone_compare(self, cloneobj, outbase, clone_disks_file=None): - """Helps compare output from passed clone instance with an xml file""" - outfile = os.path.join(CLONEXML_DIR, outbase + "-out.xml") - - cloneobj.setup_original() - cloneobj.setup_clone() - - utils.diff_compare(cloneobj.clone_xml, outfile) - if clone_disks_file: - xml_clone_disks = "" - for i in cloneobj.clone_disks: - xml_clone_disks += i.get_vol_install().get_xml() - utils.diff_compare(xml_clone_disks, clone_disks_file) - - def _clone_define(self, filebase): - """Take the valid output xml and attempt to define it on the - connection to ensure we don't get any errors""" - outfile = os.path.join(CLONEXML_DIR, filebase + "-out.xml") - outxml = open(outfile).read() - conn = utils.URIs.open_testdriver_cached() - utils.test_create(conn, outxml) - - def testRemoteNoStorage(self): - """Test remote clone where VM has no storage that needs cloning""" - conn = utils.URIs.open_test_remote() - self._clone("nostorage", conn=conn) - self._clone("noclone-storage", conn=conn) - - def testRemoteWithStorage(self): - """ - Test remote clone with storage needing cloning. Should fail, - since libvirt has no storage clone api. - """ - conn = utils.URIs.open_test_remote() - disks = ["%s/1.img" % POOL1, "%s/2.img" % POOL1] - try: - self._clone("general-cfg", disks=disks, conn=conn) - # We shouldn't succeed, so test fails - raise AssertionError("Remote clone with storage passed " - "when it shouldn't.") - except (ValueError, RuntimeError) as e: - # Exception expected - log.debug("Received expected exception: %s", str(e)) - - def testCloneStorageManaged(self): - disks = ["%s/new1.img" % POOL1, "%s/new2.img" % DISKPOOL] - self._clone("managed-storage", disks=disks) - - def testCloneStorageCrossPool(self): - conn = utils.URIs.open_test_remote() - clone_disks_file = os.path.join( - CLONEXML_DIR, "cross-pool-disks-out.xml") - disks = ["%s/new1.img" % POOL2, "%s/new2.img" % POOL1] - self._clone("cross-pool", disks=disks, - clone_disks_file=clone_disks_file, conn=conn) - - def testCloneStorageForce(self): - disks = ["/dev/default-pool/1234.img", None, "/clone2.img"] - self._clone("force", disks=disks, force_list=["hda", "fdb", "sdb"]) - - def testCloneStorageSkip(self): - disks = ["/dev/default-pool/1234.img", None, "/tmp/clone2.img"] - skip_list = ["hda", "fdb"] - self._clone("skip", disks=disks, skip_list=skip_list) - - def testCloneFullPool(self): - with self.assertRaises(Exception): - self._clone("fullpool", - disks=["/full-pool/test.img"], compare=False) - - def testCloneNvramAuto(self): - self._clone("nvram-auto") - - def testCloneNvramNewpool(self): - self._clone("nvram-newpool") - - def testCloneNvramMissing(self): - self._clone("nvram-missing") - - def testCloneGraphicsPassword(self): - self._clone("graphics-password") - - def testCloneChannelSource(self): - self._clone("channel-source") - - def testCloneMisc(self): - conn = utils.URIs.open_testdriver_cached() - - with self.assertRaises(RuntimeError) as err: - cloner = Cloner(conn) - # Add this bit here for coverage testing - cloner.clone_xml = None - cloner.setup_original() - self.assertTrue("Original guest name or XML" in str(err.exception)) - - with self.assertRaises(RuntimeError) as err: - cloner = Cloner(conn) - cloner.original_guest = "test-snapshots" - cloner.setup_original() - self.assertTrue("must be shutoff" in str(err.exception)) - - with self.assertRaises(ValueError) as err: - cloner = Cloner(conn) - cloner.original_guest = "test-clone-simple" - cloner.setup_original() - cloner.setup_clone() - self.assertTrue("More disks to clone" in str(err.exception)) - - cloner = Cloner(conn) - self.assertEqual( - cloner.generate_clone_name("test-clone5"), "test-clone6") + assert open(tmp1.name).read() == open(inp1).read() + assert open(tmp2.name).read() == open(inp2).read() diff --git a/tests/test_xmlconfig.py b/tests/test_xmlconfig.py index 591723cc..ee430659 100644 --- a/tests/test_xmlconfig.py +++ b/tests/test_xmlconfig.py @@ -315,3 +315,15 @@ class TestXMLMisc(unittest.TestCase): newdisk.path = newdisk.path newdisk.set_local_disk_to_clone(srcdisk, True) newdisk.build_storage(None) + + newdisk = virtinst.DeviceDisk(conn) + newdisk.type = "block" + newdisk.path = "/dev/foo/idontexist" + assert newdisk.get_size() == 0 + + conn = utils.URIs.open_testdriver_cached() + volpath = "/dev/default-pool/test-clone-simple.img" + assert virtinst.DeviceDisk.path_definitely_exists(conn, volpath) + disk = virtinst.DeviceDisk(conn) + disk.path = volpath + assert disk.get_size() diff --git a/virtinst/cloner.py b/virtinst/cloner.py index 4ce56683..3c23e8cb 100644 --- a/virtinst/cloner.py +++ b/virtinst/cloner.py @@ -14,7 +14,6 @@ import libvirt from . import generatename from . import progress -from . import xmlutil from .guest import Guest from .devices import DeviceInterface from .devices import DeviceDisk @@ -49,394 +48,416 @@ def _replace_vm(conn, name): }) +def _generate_clone_name(conn, basename): + """ + If the orig name is "foo-clone", we don't want the clone to be + "foo-clone-clone", we want "foo-clone1" + """ + match = re.search("-clone[1-9]*$", basename) + start_num = 1 + force_num = False + if match: + num_match = re.search("[1-9]+$", match.group()) + if num_match: + start_num = int(str(num_match.group())) + 1 + force_num = True + basename = basename.replace(match.group(), "") + + def cb(n): + return generatename.check_libvirt_collision( + conn.lookupByName, n) + basename = basename + "-clone" + return generatename.generate_name(basename, cb, + sep="", start_num=start_num, force_num=force_num) + + +def _generate_clone_disk_path(conn, origname, newname, origpath): + """ + Generate desired cloned disk path name, derived from the + original path, original VM name, and proposed new VM name + """ + if origpath is None: + return None + + path = origpath + suffix = "" + + # Try to split the suffix off the existing disk name. Ex. + # foobar.img -> foobar-clone.img + # + # If the suffix is greater than 7 characters, assume it isn't + # a file extension and is part of the disk name, at which point + # just stick '-clone' on the end. + if "." in origpath and len(origpath.rsplit(".", 1)[1]) <= 7: + path, suffix = origpath.rsplit(".", 1) + suffix = "." + suffix + + dirname = os.path.dirname(path) + basename = os.path.basename(path) + + clonebase = basename + "-clone" + if origname and basename == origname: + clonebase = newname + + clonebase = os.path.join(dirname, clonebase) + def cb(p): + return DeviceDisk.path_definitely_exists(conn, p) + return generatename.generate_name(clonebase, cb, suffix=suffix) + + +def _lookup_vm(conn, name): + try: + return conn.lookupByName(name) + except libvirt.libvirtError: + e = ValueError(_("Domain '%s' was not found.") % str(name)) + raise e from None + + +def _build_clone_vol_install(orig_disk, clone_disk): + vol_install = DeviceDisk.build_vol_install( + orig_disk.conn, os.path.basename(clone_disk.path), + clone_disk.get_parent_pool(), .000001, False) + vol_install.input_vol = orig_disk.get_vol_object() + + # Source and dest are managed. If they share the same pool, + # replace vol_install with a CloneVolume instance, otherwise + # simply set input_vol on the dest vol_install + if (vol_install.pool.name() == + orig_disk.get_parent_pool().name()): + vol_install.sync_input_vol() + else: + # Cross pool cloning + # Sync only the format of the image. + vol_install.sync_input_vol(only_format=True) + + return vol_install + + +def _build_clone_disk(orig_disk, clonepath, allow_create, sparse): + conn = orig_disk.conn + device = DeviceDisk.DEVICE_DISK + if not clonepath: + device = DeviceDisk.DEVICE_CDROM + + clone_disk = DeviceDisk(conn) + clone_disk.path = clonepath + clone_disk.device = device + + if not allow_create: + clone_disk.validate() + return clone_disk + + if clone_disk.get_vol_object(): + # Special case: non remote cloning of a guest using + # managed block devices: fall back to local cloning if + # we have permissions to do so. This validation check + # caused a few bug reports in a short period of time, + # so must be a common case. + if (conn.is_remote() or + clone_disk.type != clone_disk.TYPE_BLOCK or + not orig_disk.path or + not os.access(orig_disk.path, os.R_OK) or + not clone_disk.path or + not os.access(clone_disk.path, os.W_OK)): + raise RuntimeError( + _("Clone onto existing storage volume is not " + "currently supported: '%s'") % clone_disk.path) + + if (orig_disk.get_vol_object() and + clone_disk.wants_storage_creation()): + vol_install = _build_clone_vol_install(orig_disk, clone_disk) + if not sparse: + vol_install.allocation = vol_install.capacity + clone_disk.set_vol_install(vol_install) + elif orig_disk.path: + clone_disk.set_local_disk_to_clone(orig_disk, sparse) + + clone_disk.validate() + return clone_disk + + +class _CloneDiskInfo: + """ + Class that tracks some additional information about how we want + to default handle each disk of the source VM + """ + def __init__(self, srcdisk): + self.disk = DeviceDisk(srcdisk.conn, parsexml=srcdisk.get_xml()) + self._do_clone = self._do_we_clone_default() + self.clone_disk = None + + def is_clone_requested(self): + return self._do_clone + def set_clone_requested(self, val): + self._do_clone = val + + def _do_we_clone_default(self): + if not self.disk.path: + return False + if self.disk.read_only: + return False + if self.disk.shareable: + return False + return True + + def check_clonable(self): + try: + # This forces DeviceDisk to resolve the storage backend + self.disk.path = self.disk.path + if self.disk.wants_storage_creation(): + raise ValueError( + _("Disk path '%s' does not exist.") % self.disk.path) + except Exception as e: + log.debug("Exception processing clone original path", exc_info=True) + err = _("Could not determine original disk information: %s" % str(e)) + raise ValueError(err) from None + + def set_clone_path(self, path, allow_create, sparse): + if allow_create: + self.check_clonable() + + try: + self.clone_disk = _build_clone_disk( + self.disk, path, allow_create, sparse) + except Exception as e: + log.debug("Error setting clone path.", exc_info=True) + raise ValueError( + _("Could not use path '%(path)s' for cloning: %(error)s") % { + "path": path, + "error": str(e), + }) + + class Cloner(object): + @staticmethod + def generate_clone_name(conn, basename): + return _generate_clone_name(conn, basename) - # Reasons why we don't default to cloning. - CLONE_POLICY_NO_READONLY = 1 - CLONE_POLICY_NO_SHAREABLE = 2 - CLONE_POLICY_NO_EMPTYMEDIA = 3 + @staticmethod + def generate_clone_disk_path(conn, origname, newname, origpath): + return _generate_clone_disk_path(conn, origname, newname, origpath) - def __init__(self, conn): + def __init__(self, conn, src_name=None, src_xml=None): self.conn = conn - # original guest name or uuid - self._original_guest = None - self.original_dom = None - self._original_disks = [] - self._original_xml = None - self._guest = None + self._src_guest = None + self._new_guest = None + self._diskinfos = [] + self._init_src(src_name, src_xml) - # clone guest - self._clone_name = None - self._clone_disks = [] - self._clone_macs = [] - self._clone_uuid = None - self._clone_sparse = True - self._clone_xml = None - self.clone_nvram = None + self._new_nvram_path = None self._nvram_disk = None - self._force_target = [] - self._skip_target = [] - self._preserve = True - self._clone_running = False + self._sparse = True + self._overwrite = True self._replace = False self._reflink = False - # Default clone policy for back compat: don't clone readonly, - # shareable, or empty disks - self._clone_policy = [] - self.clone_policy = [self.CLONE_POLICY_NO_READONLY, - self.CLONE_POLICY_NO_SHAREABLE, - self.CLONE_POLICY_NO_EMPTYMEDIA] - # Generate a random UUID at the start - self.clone_uuid = Guest.generate_uuid(conn) + ################# + # Init routines # + ################# + + def _init_src(self, src_name, src_xml): + """ + Set up the source VM info we are cloning, from passed in VM name + or full XML + """ + if not src_xml: + dom = _lookup_vm(self.conn, src_name) + status = dom.info()[0] + if status not in [libvirt.VIR_DOMAIN_SHUTOFF]: + raise RuntimeError(_("Domain to clone must be shutoff.")) + flags = libvirt.VIR_DOMAIN_XML_SECURE + src_xml = dom.XMLDesc(flags) + + log.debug("Original XML:\n%s", src_xml) + + self._src_guest = Guest(self.conn, parsexml=src_xml) + self._new_guest = Guest(self.conn, parsexml=src_xml) + self._init_new_guest() + + # Collect disk info for every disk to determine if we will + # default to cloning or not + for disk in self._src_guest.devices.disk: + self._diskinfos.append(_CloneDiskInfo(disk)) + for diskinfo in [d for d in self._diskinfos if d.is_clone_requested()]: + disk = diskinfo.disk + log.debug("Wants cloning: size=%s path=%s", + disk.get_size(), disk.path) + + def _init_new_guest(self): + """ + Perform the series of unconditional new VM changes we always make + """ + self._new_guest.id = None + self._new_guest.title = None + self._new_guest.uuid = None + self._new_guest.uuid = Guest.generate_uuid(self.conn) + + for dev in self._new_guest.devices.graphics: + if dev.port and dev.port != -1: + log.warning(_("Setting the graphics device port to autoport, " + "in order to avoid conflicting.")) + dev.port = -1 + + for iface in self._new_guest.devices.interface: + iface.target_dev = None + iface.macaddr = DeviceInterface.generate_mac(self.conn) + + # For guest agent channel, remove a path to generate a new one with + # new guest name + for channel in self._new_guest.devices.channel: + if (channel.type == DeviceChannel.TYPE_UNIX and + channel.target_name and channel.source.path and + channel.target_name in channel.source.path): + channel.source.path = None + + new_name = Cloner.generate_clone_name(self.conn, self.src_name) + log.debug("Auto-generated clone name '%s'", new_name) + self.set_clone_name(new_name) ############## # Properties # ############## - # Original guest name - def get_original_guest(self): - return self._original_guest - def set_original_guest(self, original_guest): - if self._lookup_vm(original_guest): - self._original_guest = original_guest - original_guest = property(get_original_guest, set_original_guest) + @property + def src_name(self): + """ + The name of the original VM we are cloning + """ + return self._src_guest.name - # XML of the original guest - def set_original_xml(self, val): - self._original_xml = val - self._original_guest = Guest(self.conn, - parsexml=self._original_xml).name - def get_original_xml(self): - return self._original_xml - original_xml = property(get_original_xml, set_original_xml) + @property + def new_guest(self): + """ + The Guest instance of the new XML we will create + """ + return self._new_guest - # Name to use for the new guest clone - def get_clone_name(self): - return self._clone_name def set_clone_name(self, name): - try: - Guest.validate_name(self.conn, name, - check_collision=not self.replace, - validate=False) - except ValueError as e: - raise ValueError(_("Invalid name for new guest: %s") % e) + self._new_guest.name = name - self._clone_name = name - clone_name = property(get_clone_name, set_clone_name) - - # UUID to use for the new guest clone def set_clone_uuid(self, uuid): - self._clone_uuid = uuid - def get_clone_uuid(self): - return self._clone_uuid - clone_uuid = property(get_clone_uuid, set_clone_uuid) + """ + Override the new VMs generated UUId + """ + self._new_guest.uuid = uuid - # Paths to use for the new disk locations - def set_clone_paths(self, paths): - disklist = [] - for path in xmlutil.listify(paths): - try: - device = DeviceDisk.DEVICE_DISK - if not path: - device = DeviceDisk.DEVICE_CDROM - - disk = DeviceDisk(self.conn) - disk.path = path - disk.device = device - - if (not self.preserve_dest_disks and - disk.wants_storage_creation()): - vol_install = DeviceDisk.build_vol_install( - self.conn, os.path.basename(disk.path), - disk.get_parent_pool(), .000001, False) - disk.set_vol_install(vol_install) - disk.validate() - disklist.append(disk) - except Exception as e: - log.debug("Error setting clone path.", exc_info=True) - raise ValueError( - _("Could not use path '%(path)s' for cloning: %(error)s") % { - "path": path, - "error": str(e), - }) - - self._clone_disks = disklist - def get_clone_paths(self): - return [d.path for d in self.clone_disks] - clone_paths = property(get_clone_paths, set_clone_paths) - - # DeviceDisk instances for the new disk paths - @property - def clone_disks(self): - return self._clone_disks - - # MAC address for the new guest clone - def set_clone_macs(self, mac): - self._clone_macs = xmlutil.listify(mac) - def get_clone_macs(self): - return self._clone_macs - clone_macs = property(get_clone_macs, set_clone_macs) - - # DeviceDisk instances of the original disks being cloned - @property - def original_disks(self): - return self._original_disks - - # Generated XML for the guest clone - def get_clone_xml(self): - return self._clone_xml - def set_clone_xml(self, clone_xml): - self._clone_xml = clone_xml - clone_xml = property(get_clone_xml, set_clone_xml) - - # Whether to attempt sparse allocation during cloning - def get_clone_sparse(self): - return self._clone_sparse - def set_clone_sparse(self, flg): - self._clone_sparse = flg - clone_sparse = property(get_clone_sparse, set_clone_sparse) - - # If true, preserve ALL original disk devices - def get_preserve(self): - return self._preserve - def set_preserve(self, flg): - self._preserve = flg - preserve = property(get_preserve, set_preserve) - - # If true, preserve ALL disk devices for the NEW guest. - # This means no storage cloning. - # This is a convenience access for not Cloner.preserve - @property - def preserve_dest_disks(self): - return not self.preserve - - # List of disk targets that we force cloning despite - # Cloner's recommendation - def set_force_target(self, dev): - if isinstance(dev, list): - self._force_target = dev[:] - else: - self._force_target.append(dev) - def get_force_target(self): - return self._force_target - force_target = property(get_force_target, set_force_target) - - # List of disk targets that we skip cloning despite Cloner's - # recommendation. This takes precedence over force_target.") - def set_skip_target(self, dev): - if isinstance(dev, list): - self._skip_target = dev[:] - else: - self._skip_target.append(dev) - def get_skip_target(self): - return self._skip_target - skip_target = property(get_skip_target, set_skip_target) - - # List of policy rules for determining which vm disks to clone. - # See CLONE_POLICY_* - def set_clone_policy(self, policy_list): - self._clone_policy = policy_list - def get_clone_policy(self): - return self._clone_policy - clone_policy = property(get_clone_policy, set_clone_policy) - - # Allow cloning a running VM. If enabled, domain state is not - # checked before cloning. - def get_clone_running(self): - return self._clone_running - def set_clone_running(self, val): - self._clone_running = bool(val) - clone_running = property(get_clone_running, set_clone_running) - - # If enabled, don't check for clone name collision, simply undefine - # any conflicting guest. - def _get_replace(self): - return self._replace - def _set_replace(self, val): + def set_replace(self, val): + """ + If True, don't check for clone name collision, simply undefine + any conflicting guest. + """ self._replace = bool(val) - replace = property(_get_replace, _set_replace) - # If true, use COW lightweight copy - def _get_reflink(self): - return self._reflink - def _set_reflink(self, reflink): + def set_reflink(self, reflink): + """ + If true, use COW lightweight copy + """ self._reflink = reflink - reflink = property(_get_reflink, _set_reflink) + + def set_sparse(self, flg): + """ + If True, attempt sparse allocation during cloning + """ + self._sparse = flg + + def get_diskinfos(self): + """ + Return the list of _CloneDiskInfo instances + """ + return self._diskinfos[:] + + def get_diskinfos_to_clone(self): + """ + Return a list of _CloneDiskInfo that are tagged for cloning + """ + return [di for di in self.get_diskinfos() if di.is_clone_requested()] + + def set_nvram_path(self, val): + """ + If the VM needs to have nvram content cloned, this overrides the + destination path + """ + self._new_nvram_path = val + + def set_overwrite(self, flg): + """ + If False, no data is copied to the destination disks by default. + Storage may be created, but it is empty. + """ + self._overwrite = flg ###################### # Functional methods # ###################### - def setup_original(self): - """ - Validate and setup all parameters needed for the original (cloned) VM - """ - log.debug("Validating original guest parameters") - - if self.original_guest is None and self.original_xml is None: - raise RuntimeError(_("Original guest name or XML is required.")) - - if self.original_guest is not None and not self.original_xml: - self.original_dom = self._lookup_vm(self.original_guest) - flags = libvirt.VIR_DOMAIN_XML_SECURE - self.original_xml = self.original_dom.XMLDesc(flags) - - log.debug("Original XML:\n%s", self.original_xml) - - self._guest = Guest(self.conn, parsexml=self.original_xml) - self._guest.id = None - - # Pull clonable storage info from the original xml - self._original_disks = self._get_original_disks_info() - - log.debug("Original paths: %s", - [d.path for d in self.original_disks]) - log.debug("Original sizes: %s", - [d.get_size() for d in self.original_disks]) - - if not self.clone_running and self.original_dom: - status = self.original_dom.info()[0] - if status not in [libvirt.VIR_DOMAIN_SHUTOFF]: - raise RuntimeError(_("Domain to clone must be shutoff.")) - - def _setup_disk_clone_destination(self, orig_disk, clone_disk): - """ - Helper that validates the new path location - """ - if self.preserve_dest_disks: - return - - if clone_disk.get_vol_object(): - # Special case: non remote cloning of a guest using - # managed block devices: fall back to local cloning if - # we have permissions to do so. This validation check - # caused a few bug reports in a short period of time, - # so must be a common case. - if (self.conn.is_remote() or - clone_disk.type != clone_disk.TYPE_BLOCK or - not orig_disk.path or - not os.access(orig_disk.path, os.R_OK) or - not clone_disk.path or - not os.access(clone_disk.path, os.W_OK)): - raise RuntimeError( - _("Clone onto existing storage volume is not " - "currently supported: '%s'") % clone_disk.path) - - # Setup proper cloning inputs for the new virtual disks - if (orig_disk.get_vol_object() and - clone_disk.get_vol_install()): - clone_vol_install = clone_disk.get_vol_install() - - # Source and dest are managed. If they share the same pool, - # replace vol_install with a CloneVolume instance, otherwise - # simply set input_vol on the dest vol_install - if (clone_vol_install.pool.name() == - orig_disk.get_parent_pool().name()): - vol_install = StorageVolume(self.conn) - vol_install.input_vol = orig_disk.get_vol_object() - vol_install.sync_input_vol() - vol_install.name = clone_vol_install.name - else: - # Cross pool cloning - # Sync only the format of the image. - clone_vol_install.input_vol = orig_disk.get_vol_object() - vol_install = clone_vol_install - vol_install.input_vol = orig_disk.get_vol_object() - vol_install.sync_input_vol(only_format=True) - - if not self.clone_sparse: - vol_install.allocation = vol_install.capacity - vol_install.reflink = self.reflink - clone_disk.set_vol_install(vol_install) - elif orig_disk.path: - clone_disk.set_local_disk_to_clone(orig_disk, self.clone_sparse) - - clone_disk.validate() - - def _prepare_nvram(self): - if self.clone_nvram is None: - nvram_dir = os.path.dirname(self._guest.os.nvram) - self.clone_nvram = os.path.join(nvram_dir, - "%s_VARS.fd" % self._clone_name) + new_nvram_path = self._new_nvram_path + if new_nvram_path is None: + nvram_dir = os.path.dirname(self._new_guest.os.nvram) + new_nvram_path = os.path.join( + nvram_dir, "%s_VARS.fd" % self._new_guest.name) old_nvram = DeviceDisk(self.conn) - old_nvram.path = self._guest.os.nvram - + old_nvram.path = self._new_guest.os.nvram nvram = DeviceDisk(self.conn) - nvram.path = self.clone_nvram + nvram.path = new_nvram_path + diskinfo = _CloneDiskInfo(old_nvram) + allow_create = self._overwrite - if (not self.preserve_dest_disks and + if (allow_create and nvram.wants_storage_creation() and old_nvram.get_vol_object()): + # We only run validation if there's some existing nvram we + # can copy. It's valid for nvram to not exist at VM define + # time, libvirt will create it for us + diskinfo.set_clone_path(new_nvram_path, allow_create, self._sparse) + self._nvram_disk = diskinfo.clone_disk + self._nvram_disk.get_vol_install().reflink = self._reflink - nvram_install = DeviceDisk.build_vol_install( - self.conn, os.path.basename(nvram.path), - nvram.get_parent_pool(), nvram.get_size(), False) - nvram_install.input_vol = old_nvram.get_vol_object() - nvram_install.sync_input_vol(only_format=True) - nvram_install.reflink = self.reflink - nvram.set_vol_install(nvram_install) - - nvram.validate() - self._nvram_disk = nvram - - self._guest.os.nvram = nvram.path + self._new_guest.os.nvram = nvram.path - def setup_clone(self): + def prepare(self): """ Validate and set up all parameters needed for the new (clone) VM """ - log.debug("Validating clone parameters.") + try: + Guest.validate_name(self.conn, self._new_guest.name, + check_collision=not self._replace, + validate=False) + except ValueError as e: + raise ValueError(_("Invalid name for new guest: %s") % e) - self._clone_xml = self.original_xml + for diskinfo in self.get_diskinfos_to_clone(): + orig_disk = diskinfo.disk - if len(self.clone_disks) < len(self.original_disks): - raise ValueError(_("More disks to clone than new paths specified. " - "(%(passed)d specified, %(need)d needed") % - {"passed": len(self.clone_disks), - "need": len(self.original_disks)}) + if not diskinfo.clone_disk: + # User didn't set a path, generate one + newpath = Cloner.generate_clone_disk_path( + self.conn, self.src_name, + self.new_guest.name, + orig_disk.path) + diskinfo.set_clone_path(newpath, + self._overwrite, self._sparse) - log.debug("Clone paths: %s", [d.path for d in self.clone_disks]) + clone_disk = diskinfo.clone_disk + assert clone_disk + log.debug("Cloning srcpath=%s dstpath=%s", + orig_disk.path, clone_disk.path) - self._guest.name = self._clone_name - self._guest.uuid = self._clone_uuid - self._guest.title = None + if self._reflink: + vol_install = clone_disk.get_vol_install() + vol_install.reflink = self._reflink - self._clone_macs.reverse() - for dev in self._guest.devices.graphics: - if dev.port and dev.port != -1: - log.warning(_("Setting the graphics device port to autoport, " - "in order to avoid conflicting.")) - dev.port = -1 - - clone_macs = self._clone_macs[:] - for iface in self._guest.devices.interface: - iface.target_dev = None - - if clone_macs: - mac = clone_macs.pop() - else: - mac = DeviceInterface.generate_mac(self.conn) - iface.macaddr = mac - - # Changing storage XML - for i, orig_disk in enumerate(self._original_disks): - clone_disk = self._clone_disks[i] - - for disk in self._guest.devices.disk: + for disk in self._new_guest.devices.disk: if disk.target == orig_disk.target: xmldisk = disk - self._setup_disk_clone_destination(orig_disk, clone_disk) - # Change the XML xmldisk.path = None xmldisk.type = clone_disk.type @@ -444,20 +465,11 @@ class Cloner(object): xmldisk.driver_type = orig_disk.driver_type xmldisk.path = clone_disk.path - # For guest agent channel, remove a path to generate a new one with - # new guest name - for channel in self._guest.devices.channel: - if (channel.type == DeviceChannel.TYPE_UNIX and - channel.target_name and channel.source.path and - channel.target_name in channel.source.path): - channel.source.path = None - - if self._guest.os.nvram: + if self._new_guest.os.nvram: self._prepare_nvram() # Save altered clone xml - self._clone_xml = self._guest.get_xml() - log.debug("Clone guest xml is\n%s", self._clone_xml) + log.debug("Clone guest xml is\n%s", self._new_guest.get_xml()) def start_duplicate(self, meter=None): """ @@ -470,14 +482,15 @@ class Cloner(object): dom = None try: # Replace orig VM if required - if self.replace: - _replace_vm(self.conn, self.clone_name) + if self._replace: + _replace_vm(self.conn, self._new_guest.name) # Define domain early to catch any xml errors before duping storage - dom = self.conn.defineXML(self.clone_xml) + dom = self.conn.defineXML(self._new_guest.get_xml()) - if self.preserve: - for dst_dev in self.clone_disks: + if self._overwrite: + diskinfos = self.get_diskinfos_to_clone() + for dst_dev in [d.clone_disk for d in diskinfos]: dst_dev.build_storage(meter) if self._nvram_disk: self._nvram_disk.build_storage(meter) @@ -488,134 +501,3 @@ class Cloner(object): raise log.debug("Duplicating finished.") - - def generate_clone_disk_path(self, origpath, newname=None): - origname = self.original_guest - newname = newname or self.clone_name - path = origpath - suffix = "" - - # Try to split the suffix off the existing disk name. Ex. - # foobar.img -> foobar-clone.img - # - # If the suffix is greater than 7 characters, assume it isn't - # a file extension and is part of the disk name, at which point - # just stick '-clone' on the end. - if "." in origpath and len(origpath.rsplit(".", 1)[1]) <= 7: - path, suffix = origpath.rsplit(".", 1) - suffix = "." + suffix - - dirname = os.path.dirname(path) - basename = os.path.basename(path) - - clonebase = basename + "-clone" - if origname and basename == origname: - clonebase = newname - - clonebase = os.path.join(dirname, clonebase) - def cb(p): - return DeviceDisk.path_definitely_exists(self.conn, p) - return generatename.generate_name(clonebase, cb, suffix=suffix) - - def generate_clone_name(self, basename=None): - # If the orig name is "foo-clone", we don't want the clone to be - # "foo-clone-clone", we want "foo-clone1" - if not basename: - basename = self.original_guest - - match = re.search("-clone[1-9]*$", basename) - start_num = 1 - force_num = False - if match: - num_match = re.search("[1-9]+$", match.group()) - if num_match: - start_num = int(str(num_match.group())) + 1 - force_num = True - basename = basename.replace(match.group(), "") - - def cb(n): - return generatename.check_libvirt_collision( - self.conn.lookupByName, n) - basename = basename + "-clone" - return generatename.generate_name(basename, cb, - sep="", start_num=start_num, force_num=force_num) - - - ############################ - # Private helper functions # - ############################ - - # Parse disk paths that need to be cloned from the original guest's xml - # Return a list of DeviceDisk instances pointing to the original - # storage - def _get_original_disks_info(self): - clonelist = [] - retdisks = [] - - for disk in self._guest.devices.disk: - if self._do_we_clone_device(disk): - clonelist.append(disk) - continue - - # Set up virtual disk to encapsulate all relevant path info - for disk in clonelist: - validate = not self.preserve_dest_disks - - try: - device = DeviceDisk.DEVICE_DISK - if not disk.path: - # Tell DeviceDisk we are a cdrom to allow empty media - device = DeviceDisk.DEVICE_CDROM - - newd = DeviceDisk(self.conn) - newd.path = disk.path - newd.device = device - newd.driver_name = disk.driver_name - newd.driver_type = disk.driver_type - newd.target = disk.target - if validate: - if newd.wants_storage_creation(): - raise ValueError(_("Disk path '%s' does not exist.") % - newd.path) - except Exception as e: - log.debug("Exception creating clone disk objects", - exc_info=True) - raise ValueError(_("Could not determine original disk " - "information: %s" % str(e))) - retdisks.append(newd) - - return retdisks - - # Pull disk #i from the original guest xml, return it's source path - # if it should be cloned - # Cloning policy based on 'clone_policy', 'force_target' and 'skip_target' - def _do_we_clone_device(self, disk): - if disk.target in self.skip_target: - return False - - if disk.target in self.force_target: - return True - - # No media path - if (not disk.path and - self.CLONE_POLICY_NO_EMPTYMEDIA in self.clone_policy): - return False - - # Readonly disks - if (disk.read_only and - self.CLONE_POLICY_NO_READONLY in self.clone_policy): - return False - - # Shareable disks - if (disk.shareable and - self.CLONE_POLICY_NO_SHAREABLE in self.clone_policy): - return False - - return True - - # Simple wrapper for checking a vm exists and returning the domain - def _lookup_vm(self, name): - try: - return self.conn.lookupByName(name) - except libvirt.libvirtError: - raise ValueError(_("Domain '%s' was not found.") % str(name)) diff --git a/virtinst/diskbackend.py b/virtinst/diskbackend.py index e384e467..3fdae5f0 100644 --- a/virtinst/diskbackend.py +++ b/virtinst/diskbackend.py @@ -517,7 +517,7 @@ class CloneStorageCreator(_StorageCreator): if self.get_dev_type() == "block": avail = _get_size(self._path) # pragma: no cover else: - vfs = os.statvfs(os.path.dirname(self._path)) + vfs = os.statvfs(os.path.dirname(os.path.abspath(self._path))) avail = vfs.f_frsize * vfs.f_bavail need = int(self._size) * 1024 * 1024 * 1024 if need > avail: # pragma: no cover diff --git a/virtinst/virtclone.py b/virtinst/virtclone.py index 961cc0ab..d05c49ab 100644 --- a/virtinst/virtclone.py +++ b/virtinst/virtclone.py @@ -14,71 +14,54 @@ from .cloner import Cloner from .logger import log -# General input gathering functions -def get_clone_name(new_name, auto_clone, design): - if not new_name and auto_clone: - # Generate a name to use - new_name = design.generate_clone_name() - log.debug("Auto-generated clone name '%s'", new_name) - - if not new_name: - fail(_("A name is required for the new virtual machine," - " use '--name NEW_VM_NAME' to specify one.")) - design.clone_name = new_name - - -def get_original_guest(guest_name, origfile, design): - origxml = None - if origfile: - f = open(origfile, "r") - origxml = f.read() - f.close() - - try: - design.original_xml = origxml - return - except (ValueError, RuntimeError) as e: # pragma: no cover - fail(e) - - if not guest_name: +def _process_src(options): + src_name = options.src_name + src_xml = None + if options.original_xml: + src_xml = open(options.original_xml).read() + elif not src_name: fail(_("An original machine name is required," - " use '--original ORIGINAL_GUEST' and try again.")) - design.original_guest = guest_name + " use '--original src_name' and try again.")) + return src_name, src_xml -def get_clone_macaddr(new_mac, design): - if new_mac is None or new_mac[0] == "RANDOM": +def _process_macs(options, cloner): + new_macs = options.new_mac + if not new_macs or new_macs[0] == "RANDOM": return - design.clone_macs = new_mac - for mac in design.clone_macs: - cli.validate_mac(design.conn, mac) + for mac in new_macs: + cli.validate_mac(cloner.conn, mac) + + for iface in cloner.new_guest.devices.interface[:]: + iface.macaddr = new_macs.pop(0) -def get_clone_diskfile(new_diskfiles, design, preserve, auto_clone): - if new_diskfiles is None: - new_diskfiles = [None] +def _process_disks(options, cloner): + newpaths = (options.new_diskfile or [])[:] - newidx = 0 - clonepaths = [] - for origpath in [d.path for d in design.original_disks]: - if len(new_diskfiles) <= newidx: - # Extend the new/passed paths list with None if it's not - # long enough - new_diskfiles.append(None) - newpath = new_diskfiles[newidx] + diskinfos = cloner.get_diskinfos_to_clone() + for diskinfo in diskinfos: + origpath = diskinfo.disk.path + newpath = None + if newpaths: + newpath = newpaths.pop(0) + elif options.auto_clone: + break if origpath is None: newpath = None - elif newpath is None and auto_clone: - newpath = design.generate_clone_disk_path(origpath) + allow_create = options.overwrite + diskinfo.set_clone_path(newpath, allow_create, options.sparse) - clonepaths.append(newpath) - newidx += 1 - design.clone_paths = clonepaths - for disk in design.clone_disks: - cli.validate_disk(disk, warn_overwrite=not preserve) +def _validate_disks(options, cloner): + # Extra CLI validation for specified disks + warn_overwrite = options.overwrite + for diskinfo in cloner.get_diskinfos(): + if not diskinfo.clone_disk: + continue + cli.validate_disk(diskinfo.clone_disk, warn_overwrite=warn_overwrite) def parse_args(): @@ -93,7 +76,7 @@ def parse_args(): cli.add_connect_option(parser) geng = parser.add_argument_group(_("General Options")) - geng.add_argument("-o", "--original", dest="original_guest", + geng.add_argument("-o", "--original", dest="src_name", help=_("Name of the original guest to clone.")) geng.add_argument("--original-xml", help=_("XML file to use as the original guest.")) @@ -121,10 +104,12 @@ def parse_args(): default=True, help=_("Do not use a sparse file for the clone's " "disk image")) - stog.add_argument("--preserve-data", action="store_false", - dest="preserve", default=True, - help=_("Do not clone storage, new disk images specified " - "via --file are preserved unchanged")) + stog.add_argument("--preserve-data", dest="overwrite", + action="store_false", default=True, + help=_("Do not clone storage contents to specified file paths, " + "their contents will be left untouched. " + "This requires specifying existing paths for " + "every clonable disk image.")) stog.add_argument("--nvram", dest="new_nvram", help=_("New file to use as storage for nvram VARS")) @@ -136,9 +121,7 @@ def parse_args(): misc = parser.add_argument_group(_("Miscellaneous Options")) # Just used for clone tests - misc.add_argument("--clone-running", action="store_true", - default=False, help=argparse.SUPPRESS) - misc.add_argument("--__test-nodry", action="store_true", + misc.add_argument("--__test-nodry", action="store_true", dest="test_nodry", default=False, help=argparse.SUPPRESS) cli.add_misc_options(misc, prompt=True, replace=True, printxml=True) @@ -148,7 +131,6 @@ def parse_args(): return parser.parse_args() - def main(conn=None): cli.earlyLogging() options = parse_args() @@ -167,47 +149,49 @@ def main(conn=None): fail(_("Either --auto-clone or --file is required," " use '--auto-clone or --file' and try again.")) - design = Cloner(conn) + src_name, src_xml = _process_src(options) + cloner = Cloner(conn, src_name, src_xml) - design.clone_running = options.clone_running - design.replace = bool(options.replace) - get_original_guest(options.original_guest, options.original_xml, - design) - get_clone_name(options.new_name, options.auto_clone, design) + cloner.set_replace(bool(options.replace)) + cloner.set_reflink(bool(options.reflink)) + cloner.set_sparse(bool(options.sparse)) + cloner.set_overwrite(bool(options.overwrite)) - get_clone_macaddr(options.new_mac, design) if options.new_uuid is not None: - design.clone_uuid = options.new_uuid - if options.reflink is True: - design.reflink = True - for i in options.target or []: - design.force_target = i - for i in options.skip_copy or []: - design.skip_target = i - design.clone_sparse = options.sparse - design.preserve = options.preserve + cloner.set_clone_uuid(options.new_uuid) + if options.new_nvram: + cloner.set_nvram_path(options.new_nvram) - design.clone_nvram = options.new_nvram + force_targets = options.target or [] + skip_targets = options.skip_copy or [] + for diskinfo in cloner.get_diskinfos(): + if diskinfo.disk.target in force_targets: + diskinfo.set_clone_requested(True) + if diskinfo.disk.target in skip_targets: + diskinfo.set_clone_requested(False) - # This determines the devices that need to be cloned, so that - # get_clone_diskfile knows how many new disk paths it needs - design.setup_original() + if options.new_name: + cloner.set_clone_name(options.new_name) + elif not options.auto_clone: + fail(_("A name is required for the new virtual machine," + " use '--name NEW_VM_NAME' to specify one.")) - get_clone_diskfile(options.new_diskfile, design, - not options.preserve, options.auto_clone) + _process_macs(options, cloner) + _process_disks(options, cloner) - # setup design object - design.setup_clone() + cloner.prepare() + + _validate_disks(options, cloner) run = True if options.xmlonly: - run = options.__test_nodry - print_stdout(design.clone_xml, do_force=True) + run = options.test_nodry + print_stdout(cloner.new_guest.get_xml(), do_force=True) if run: - design.start_duplicate(cli.get_meter()) + cloner.start_duplicate(cli.get_meter()) print_stdout("") - print_stdout(_("Clone '%s' created successfully.") % design.clone_name) + print_stdout(_("Clone '%s' created successfully.") % cloner.new_guest.name) log.debug("end clone") return 0