Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion examples/vm_os_upgrade/05_clone_from_source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
- ansible.builtin.assert:
that:
- vm_cloned is changed
- vm_cloned.msg == "Virtual machine - {{ source_vm_name }} - cloning complete to - {{ cloned_vm_name }}."
- vm_cloned.msg == expected_clone_msg
vars:
expected_clone_msg: >-
Virtual machine - {{ source_vm_name }} - cloning complete to -
{{ cloned_vm_name }} and boot order was set - you can change it with
vm_boot_devices module.

- name: Get info about VM {{ cloned_vm_name }}
scale_computing.hypercore.vm_info:
Expand Down
31 changes: 30 additions & 1 deletion plugins/module_utils/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def __init__(
power_state=None,
power_action=None,
nics=None, # nics represents a list of type Nic
disks=None, # disks represents a list of type Nic
disks=None, # disks represents a list of type Disk
# boot_devices are stored as list of nics and/or disks internally.
boot_devices=None,
attach_guest_tools_iso=False,
Expand Down Expand Up @@ -423,6 +423,27 @@ def create_export_or_import_vm_payload(ansible_dict, cloud_init, is_export):
payload["template"]["cloudInitData"] = cloud_init
return payload

@staticmethod
def clone_add_user_data_to_cloud_init(cloud_init):
# Task 370 - the generated cloud-init should include a runcmd
# to fix the grub UUID issue at first boot

user_data = cloud_init.get("user_data")
if not user_data:
return cloud_init

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to fix grub config in this case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure, this is from the task description: "If the module accepts cloud_init.user_data, the generated cloud-init should include a runcmd to fix the grub UUID issue at first boot".
I read it as "if the the user_data was passed"


cloud_init["user_data"] = (
user_data.rstrip()
+ """

runcmd:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might end adding a user_data section when we already have a user_data section.
We can add a unit tests for those corner cases, then refaction this function a bit.
Corner cases:

  • no user_data
  • emtpy user_data
  • no user_data.runcmd
  • emtpy user_data.runcmd
  • user_data.runcmd with entries

I guess we want to always comment out GRUB_DISABLE_LINUX_UUID ?

- sed -i 's/^GRUB_DISABLE_LINUX_UUID=true/#GRUB_DISABLE_LINUX_UUID=true/' /etc/default/grub
- update-grub
"""
)

return cloud_init

@classmethod
def create_clone_vm_payload(
cls,
Expand All @@ -446,6 +467,7 @@ def create_clone_vm_payload(
hypercore_tags.append(tag)
data["template"]["tags"] = ",".join(hypercore_tags)
if cloud_init:
cloud_init = cls.clone_add_user_data_to_cloud_init(cloud_init)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have impression we want to alway run this?

data["template"]["cloudInitData"] = cloud_init
if preserve_mac_address:
data["template"]["netDevs"] = [
Expand Down Expand Up @@ -610,6 +632,13 @@ def find_disk(self, slot):
if disk.slot == slot:
return disk

# primary disk is the largest Virtio disk
def get_primary_disk(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could clone a VM with IDE or SCSI disk(s).

Not sure why is the largest disk the bootable one. VM with 100 kB cloud-init ISO, 20 GB OS disk, 100 GB installed-apps disk. Maybe the first disk on bus is the OS disk, but if we add/remove disk, we likely can change that too. :( :(

Ideally we could ask HC3 about this setting for the source VM. But source VM might never be booted before clonning.

Or, we could clone VM, boot it, then assuming cloned VM does boot (really boot, not just wait on
"no OS installed") set this from cloned VM runtime state.

A NIC could be a boot device too.

@domendobnikar domendobnikar Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a cloud-image VM cloned via this module boots correctly for years while it has exactly one virtio disk, then bricks on the first reboot after any tool (CSI driver, manual disk-add, Terraform provider, or even another playbook in the same suite) attaches a second virtio disk.
From my understanding this issue is a corner case to solve the "one virtio disk already exists and is used for boot but isn't specified as one and causes issues later on when another disk is added"

virtio_disks = [disk for disk in self.disk_list if disk.type == "virtio_disk"]
if not virtio_disks:
return None
return max(virtio_disks, key=lambda disk: disk.size)

def post_vm_payload(self, rest_client, ansible_dict):
# The rest of the keys from VM_PAYLOAD_KEYS will get set properly automatically
# Cloud init will be obtained through ansible_dict - If method will be reused outside of vm module,
Expand Down
27 changes: 23 additions & 4 deletions plugins/modules/vm_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,29 @@ def run(module, rest_client):
TaskTag.wait_task(rest_client, task)
task_status = TaskTag.get_task_status(rest_client, task)
if task_status and task_status.get("state", "") == "COMPLETE":
return (
True,
f"Virtual machine - {module.params['source_vm_name']} - cloning complete to - {module.params['vm_name']}.",
)
# Get cloned VM
virtual_machine_cloned_obj = VM.get_or_fail(query={"name": module.params["vm_name"]}, rest_client=rest_client)[
0
]
# Set boot devices after cloning Issue-370 (VM starts failing as soon as another disk is attahed if boot is not specified)
# By default we always set the largest Virtio disk which is the "primary disk"
primary_disk = virtual_machine_cloned_obj.get_primary_disk()
boot_items = [primary_disk.to_ansible()] if primary_disk else []
# previous boot order after cloning is always empty
previous_boot_order = []
changed = virtual_machine_cloned_obj.set_boot_devices(boot_items, module, rest_client, previous_boot_order)
Comment thread
domendobnikar marked this conversation as resolved.
if changed:
msg = "and boot order was set - you can change it with vm_boot_devices module"
return (
True,
f"Virtual machine - {module.params['source_vm_name']} - cloning complete to - {module.params['vm_name']} {msg}.",
)
else:
msg = "and boot order was not set - you can set it with vm_boot_devices module"
return (
True,
f"Virtual machine - {module.params['source_vm_name']} - cloning complete to - {module.params['vm_name']} {msg}.",
)
raise errors.ScaleComputingError(
f"There was a problem during cloning of {module.params['source_vm_name']}, cloning failed."
)
Expand Down
6 changes: 4 additions & 2 deletions tests/integration/targets/vm_clone/tasks/01_cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
nodeUUID: ""
cause: INTERNAL
register: set_to_run_task
when: source_running_info.records
when:
- source_running_info.records is defined
- source_running_info.records | length > 0

- name: Wait for the VM to be set to SHUTDOWN
scale_computing.hypercore.task_wait:
task_tag: "{{ set_to_run_task.record }}"
when: set_to_run_task.record | default("")
when: (set_to_run_task.record | default("") | string | length) > 0

- name: Delete VM XLAB-vm_clone-xyz
scale_computing.hypercore.vm:
Expand Down
8 changes: 5 additions & 3 deletions tests/integration/targets/vm_clone/tasks/10_clone_stopped.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test - cloning complete to - XLAB-vm_clone_CI-test-clone."
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test - cloning complete to - XLAB-vm_clone_CI-test-clone and boot order was set - you can change it with vm_boot_devices module."

- name: Retrieve XLAB-vm_clone_CI-test-clone
scale_computing.hypercore.vm_info:
Expand All @@ -29,11 +29,12 @@
- cloned_info.records | length == 1
- source_info.records.0.vcpu == cloned_info.records.0.vcpu
- source_info.records.0.tags != cloned_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_info.records.0.boot_devices | length
- source_info.records.0.disks | length == cloned_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_info.records.0.nics | length
- source_info.records.0.nics.0.mac != cloned_info.records.0.nics.0.mac
- source_info.records.0.node_affinity == cloned_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_info.records.0.boot_devices | length != 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to test with "==".

I guess source_info.records.0.boot_devices | length == 0?

And cloned_info.records.0.boot_devices | length was 0 before, 1 after PR?


# ----------------------------------Idempotence check------------------------------------------------------------------------
- name: Clone XLAB-vm_clone_CI-test into XLAB-vm_clone_CI-test-clone Idempotence
Expand All @@ -57,8 +58,9 @@
- cloned_info.records | length == 1
- source_info.records.0.vcpu == cloned_info.records.0.vcpu
- source_info.records.0.tags != cloned_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_info.records.0.boot_devices | length
- source_info.records.0.disks | length == cloned_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_info.records.0.nics | length
- source_info.records.0.nics.0.mac != cloned_info.records.0.nics.0.mac
- source_info.records.0.node_affinity == cloned_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_info.records.0.boot_devices | length != 0
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test - cloning complete to - XLAB-vm_clone_CI-test-cloud_init-clone."
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test - cloning complete to - XLAB-vm_clone_CI-test-cloud_init-clone and boot order was set - you can change it with vm_boot_devices module."

- name: Retrieve XLAB-vm_clone_CI-test-cloud_init-clone
scale_computing.hypercore.vm_info:
Expand All @@ -36,11 +36,12 @@
- cloned_cloud_init_info.records | length == 1
- source_info.records.0.vcpu == cloned_cloud_init_info.records.0.vcpu
- source_info.records.0.tags == cloned_cloud_init_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_cloud_init_info.records.0.boot_devices | length
- source_info.records.0.disks | length != cloned_cloud_init_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_cloud_init_info.records.0.nics | length
- source_info.records.0.nics.0.mac != cloned_cloud_init_info.records.0.nics.0.mac
- source_info.records.0.node_affinity == cloned_cloud_init_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_cloud_init_info.records.0.boot_devices | length != 0

# ----------------------------------Idempotence check------------------------------------------------------------------------
- name: Clone XLAB-vm_clone_CI-test into XLAB-vm_clone_CI-test-cloud_init-clone Idempotence
Expand Down Expand Up @@ -70,8 +71,9 @@
- cloned_cloud_init_info.records | length == 1
- source_info.records.0.vcpu == cloned_cloud_init_info.records.0.vcpu
- source_info.records.0.tags == cloned_cloud_init_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_cloud_init_info.records.0.boot_devices | length
- source_info.records.0.disks | length != cloned_cloud_init_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_cloud_init_info.records.0.nics | length
- source_info.records.0.nics.0.mac != cloned_cloud_init_info.records.0.nics.0.mac
- source_info.records.0.node_affinity == cloned_cloud_init_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_cloud_init_info.records.0.boot_devices | length != 0
8 changes: 5 additions & 3 deletions tests/integration/targets/vm_clone/tasks/12_clone_running.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test-running - cloning complete to - XLAB-vm_clone-while-running-test-clone."
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test-running - cloning complete to - XLAB-vm_clone-while-running-test-clone and boot order was set - you can change it with vm_boot_devices module."

- name: Retrieve XLAB-vm_clone-while-running-test-clone
scale_computing.hypercore.vm_info:
Expand All @@ -26,11 +26,12 @@
- cloned_while_running_info.records | length == 1
- demo_server_info.records.0.vcpu == cloned_while_running_info.records.0.vcpu
- demo_server_info.records.0.tags == cloned_while_running_info.records.0.tags
- demo_server_info.records.0.boot_devices | length == cloned_while_running_info.records.0.boot_devices | length
- demo_server_info.records.0.disks | length == cloned_while_running_info.records.0.disks | length
- demo_server_info.records.0.nics | length == cloned_while_running_info.records.0.nics | length
- demo_server_info.records.0.nics.0.mac != cloned_while_running_info.records.0.nics.0.mac
- demo_server_info.records.0.node_affinity != cloned_while_running_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_while_running_info.records.0.boot_devices | length != 0

# ----------------------------------Idempotence check------------------------------------------------------------------------
- name: Clone XLAB-vm_clone-CI_test-running into XLAB-vm_clone-while-running-test-clone Idempotence
Expand All @@ -52,8 +53,9 @@
- cloned_while_running_info.records | length == 1
- demo_server_info.records.0.vcpu == cloned_while_running_info.records.0.vcpu
- demo_server_info.records.0.tags == cloned_while_running_info.records.0.tags
- demo_server_info.records.0.boot_devices | length == cloned_while_running_info.records.0.boot_devices | length
- demo_server_info.records.0.disks | length == cloned_while_running_info.records.0.disks | length
- demo_server_info.records.0.nics | length == cloned_while_running_info.records.0.nics | length
- demo_server_info.records.0.nics.0.mac != cloned_while_running_info.records.0.nics.0.mac
- demo_server_info.records.0.node_affinity != cloned_while_running_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_while_running_info.records.0.boot_devices | length != 0
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test - cloning complete to - XLAB-vm_clone_CI-test-preserve-mac-clone."
- output.msg == "Virtual machine - XLAB-vm_clone-CI_test - cloning complete to - XLAB-vm_clone_CI-test-preserve-mac-clone and boot order was set - you can change it with vm_boot_devices module."

- name: Retrieve XLAB-vm_clone_CI-test-preserve-mac-clone
scale_computing.hypercore.vm_info:
Expand All @@ -30,11 +30,12 @@
- cloned_info.records | length == 1
- source_info.records.0.vcpu == cloned_info.records.0.vcpu
- source_info.records.0.tags != cloned_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_info.records.0.boot_devices | length
- source_info.records.0.disks | length == cloned_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_info.records.0.nics | length
- source_info.records.0.nics.0.mac == cloned_info.records.0.nics.0.mac
- source_info.records.0.node_affinity == cloned_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_info.records.0.boot_devices | length != 0

# ----------------------------------Idempotence check------------------------------------------------------------------------
- name: Clone XLAB-vm_clone_CI-test into XLAB-vm_clone_CI-test-preserve-mac-clone Idempotence
Expand All @@ -59,8 +60,9 @@
- cloned_info.records | length == 1
- source_info.records.0.vcpu == cloned_info.records.0.vcpu
- source_info.records.0.tags != cloned_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_info.records.0.boot_devices | length
- source_info.records.0.disks | length == cloned_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_info.records.0.nics | length
- source_info.records.0.nics.0.mac == cloned_info.records.0.nics.0.mac
- source_info.records.0.node_affinity == cloned_info.records.0.node_affinity
# Cloned VM's boot devices should be set
- cloned_info.records.0.boot_devices | length != 0
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone_CI-test-clone-from-snapshot-source - cloning complete to - XLAB-vm_clone_CI-test-clone-from-snapshot-clone."
- output.msg == "Virtual machine - XLAB-vm_clone_CI-test-clone-from-snapshot-source - cloning complete to - XLAB-vm_clone_CI-test-clone-from-snapshot-clone and boot order was set - you can change it with vm_boot_devices module."

- name: Assert that cloning was successful
scale_computing.hypercore.vm_info:
Expand All @@ -140,7 +140,8 @@
- vm_clone_info.records.0.memory == 511705088
- vm_clone_info.records.0.nics | length == 1
- vm_clone_info.records.0.disks | length == 1
- vm_clone_info.records.0.boot_devices | length == 0
# Boot devices should be set after cloning
- vm_clone_info.records.0.boot_devices | length != 0
- vm_clone_info.records.0.vcpu == 2
- vm_clone_info.records.0.vm_name == "XLAB-vm_clone_CI-test-clone-from-snapshot-clone"

Expand All @@ -153,7 +154,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone_CI-test-clone-from-snapshot-source - cloning complete to - XLAB-vm_clone_CI-test-clone-from-snapshot-clone-2."
- output.msg == "Virtual machine - XLAB-vm_clone_CI-test-clone-from-snapshot-source - cloning complete to - XLAB-vm_clone_CI-test-clone-from-snapshot-clone-2 and boot order was set - you can change it with vm_boot_devices module."

- name: Assert that cloning was successful and clones differ
scale_computing.hypercore.vm_info:
Expand All @@ -167,7 +168,8 @@
- vm_clone_info.records.0.memory == 511705088
- vm_clone_info.records.0.nics | length == 1
- vm_clone_info.records.0.disks | length == 2
- vm_clone_info.records.0.boot_devices | length == 0
# Boot devices should be set after cloning
- vm_clone_info.records.0.boot_devices | length != 0
- vm_clone_info.records.0.vcpu == 3
- vm_clone_info.records.0.vm_name == "XLAB-vm_clone_CI-test-clone-from-snapshot-clone-2"

Expand Down Expand Up @@ -262,7 +264,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - XLAB-vm_clone_CI-test-clone-from-snapshot-source - cloning complete to - XLAB-vm_clone_CI-test-clone-from-snapshot-clone-3."
- output.msg == "Virtual machine - XLAB-vm_clone_CI-test-clone-from-snapshot-source - cloning complete to - XLAB-vm_clone_CI-test-clone-from-snapshot-clone-3 and boot order was set - you can change it with vm_boot_devices module."

- name: Assert that cloning was successful - same label
scale_computing.hypercore.vm_info:
Expand All @@ -276,6 +278,7 @@
- vm_clone_info.records.0.memory == 511705088
- vm_clone_info.records.0.nics | length == 1
- vm_clone_info.records.0.disks | length == 3
- vm_clone_info.records.0.boot_devices | length == 0
# Boot devices should be set after cloning
- vm_clone_info.records.0.boot_devices | length != 0
- vm_clone_info.records.0.vcpu == 4
- vm_clone_info.records.0.vm_name == "XLAB-vm_clone_CI-test-clone-from-snapshot-clone-3"
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- ansible.builtin.assert:
that:
- output is changed
- output.msg == "Virtual machine - {{ vm_name_src }} - cloning complete to - {{ vm_name_dest }}."
- output.msg == "Virtual machine - {{ vm_name_src }} - cloning complete to - {{ vm_name_dest }} and boot order was set - you can change it with vm_boot_devices module."

- name: dest-cluster Retrieve {{ vm_name_dest }}
scale_computing.hypercore.vm_info:
Expand All @@ -21,7 +21,6 @@
- cloned_info.records | length == 1
- source_info.records.0.vcpu == cloned_info.records.0.vcpu
- source_info.records.0.tags != cloned_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_info.records.0.boot_devices | length
- source_info.records.0.disks | length == cloned_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_info.records.0.nics | length
- source_info.records.0.nics.0.mac != cloned_info.records.0.nics.0.mac
Expand Down Expand Up @@ -51,7 +50,6 @@
- cloned_info.records | length == 1
- source_info.records.0.vcpu == cloned_info.records.0.vcpu
- source_info.records.0.tags != cloned_info.records.0.tags
- source_info.records.0.boot_devices | length == cloned_info.records.0.boot_devices | length
- source_info.records.0.disks | length == cloned_info.records.0.disks | length
- source_info.records.0.nics | length == cloned_info.records.0.nics | length
- source_info.records.0.nics.0.mac != cloned_info.records.0.nics.0.mac
Expand Down
Loading
Loading