Skip to content

Commit 49adb55

Browse files
authored
Add new force_disk parameter for cloud_virtual_instance module (ansible-collections#224)
- Add new force_disk parameter to force deletion and recreation of existing disk files.
1 parent 306fff9 commit 49adb55

File tree

3 files changed

+224
-8
lines changed

3 files changed

+224
-8
lines changed

plugins/doc_fragments/virt_install.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,7 @@ class ModuleDocFragment(object):
13131313
description:
13141314
- Additional options for the direct attached macvtap interface.
13151315
- The dictionary contains key/value pairs that define individual properties.
1316+
version_added: '2.1.0'
13161317
target:
13171318
type: dict
13181319
description:

plugins/modules/virt_cloud_instance.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@
5656
- Only used when O(base_image) is a URL.
5757
- Useful for updating cached images to their latest versions.
5858
default: false
59+
force_disk:
60+
type: bool
61+
description:
62+
- Force deletion and recreation of existing disk files.
63+
- When set to V(true), any existing disk files specified in O(disks) will be removed and recreated.
64+
- When set to V(false), the module will fail if any disk file already exists.
65+
- This parameter only affects disk file creation, not VM definition.
66+
- Use with O(recreate=true) to recreate both the VM and its disk files.
67+
default: false
5968
image_checksum:
6069
type: str
6170
description:
@@ -245,6 +254,21 @@
245254
meta_data:
246255
instance-id: web-server-01
247256
local-hostname: web-server-01
257+
258+
# Force overwrite existing disk files
259+
- name: Create VM and force overwrite existing disk
260+
community.libvirt.virt_cloud_instance:
261+
name: test-vm
262+
base_image: /srv/images/ubuntu-22.04-server-cloudimg-amd64.img
263+
force_disk: true
264+
disks:
265+
- path: /var/lib/libvirt/images/test-vm.qcow2
266+
size: 20
267+
format: qcow2
268+
memory: 2048
269+
vcpus: 2
270+
networks:
271+
- network: default
248272
"""
249273

250274
RETURN = r"""
@@ -431,20 +455,30 @@ def _fetch_checksum_from_url(self, checksum_url, algorithm):
431455
self.module.fail_json(
432456
msg="Unable to find a checksum for file '%s' in '%s'" % (filename, checksum_url))
433457

434-
def build_system_disk(self, disk_param):
458+
def build_system_disk(self, disk_param, force_disk=False):
435459
"""
436460
Build system disk from base image.
437461
438462
This operation creates and modifies disk files, so it respects check_mode.
439463
In check_mode, validation is performed but no files are created or modified.
464+
465+
Args:
466+
disk_param: Disk configuration dictionary
467+
force_disk: If True, remove existing disk file before creating new one
440468
"""
441469
system_disk = self._resolve_system_disk(disk_param)
442470
disk_path = self.system_disk_path
443471
base_image_path = self.base_image_path
444472

445473
if os.path.exists(disk_path):
446-
self.module.fail_json(
447-
msg="The system disk file already exists: %s" % disk_path)
474+
if force_disk:
475+
# In check_mode, don't actually delete the file
476+
if not self.module.check_mode:
477+
os.remove(disk_path)
478+
self.module.log("Removed existing disk file: %s" % disk_path)
479+
else:
480+
self.module.fail_json(
481+
msg="The system disk file already exists: %s" % disk_path)
448482

449483
qemuImgTool = QemuImgTool(self.module)
450484

@@ -563,6 +597,7 @@ def core(module):
563597
base_image = module.params.get('base_image')
564598
image_cache_dir = module.params.get('image_cache_dir')
565599
force_pull = module.params.get('force_pull', False)
600+
force_disk = module.params.get('force_disk', False)
566601
disks = module.params.get('disks', [])
567602
image_checksum = module.params.get('image_checksum')
568603
url_timeout = module.params.get('url_timeout')
@@ -614,7 +649,7 @@ def core(module):
614649
if not image_operator.validate_checksum():
615650
module.fail_json(
616651
msg="The checksum of the base image does not match the expected value")
617-
system_disk = image_operator.build_system_disk(disks[0])
652+
system_disk = image_operator.build_system_disk(disks[0], force_disk=force_disk)
618653

619654
# Add base_image_path to result
620655
result['base_image_path'] = image_operator.base_image_path
@@ -672,6 +707,7 @@ def main():
672707
base_image=dict(type='str', required=True),
673708
image_cache_dir=dict(type='path'),
674709
force_pull=dict(type='bool', default=False),
710+
force_disk=dict(type='bool', default=False),
675711
image_checksum=dict(type='str'),
676712
url_timeout=dict(type='int', default=60),
677713
url_username=dict(type='str'),

tests/unit/modules/test_virt_cloud_instance.py

Lines changed: 183 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,89 @@ def test_build_system_disk_size_too_small(self, mock_exists, mock_qemu_tool_clas
699699
msg=f"The system disk size is too small to import the base image: {expected_system_size} < 21474836480"
700700
)
701701

702+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.QemuImgTool')
703+
@mock.patch('os.path.exists')
704+
@mock.patch('os.remove')
705+
def test_build_system_disk_force_disk_removes_existing(self, mock_remove, mock_exists, mock_qemu_tool_class):
706+
"""Test building system disk with force_disk=True removes existing file"""
707+
mock_exists.return_value = True # System disk exists
708+
709+
# Configure the mocked QemuImgTool
710+
mock_qemu_instance = mock.Mock()
711+
mock_qemu_tool_class.return_value = mock_qemu_instance
712+
mock_qemu_instance.info.return_value = (0, {
713+
'format': 'qcow2',
714+
'virtual-size': 1073741824 # 1GB
715+
}, '')
716+
mock_qemu_instance.resize.return_value = (0, '', '')
717+
718+
disk_param = {
719+
'path': '/var/lib/libvirt/images/vm.qcow2',
720+
'size': 20,
721+
'format': 'qcow2'
722+
}
723+
724+
result = self.operator.build_system_disk(disk_param, force_disk=True)
725+
726+
# Verify the existing file was removed
727+
mock_remove.assert_called_once_with('/var/lib/libvirt/images/vm.qcow2')
728+
self.assertEqual(result, disk_param)
729+
730+
# Verify disk was created after removal
731+
self.mock_module.preserved_copy.assert_called_once_with(
732+
'/path/to/base.qcow2', '/var/lib/libvirt/images/vm.qcow2'
733+
)
734+
mock_qemu_instance.resize.assert_called_once()
735+
736+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.QemuImgTool')
737+
@mock.patch('os.path.exists')
738+
@mock.patch('os.remove')
739+
def test_build_system_disk_force_disk_check_mode(self, mock_remove, mock_exists, mock_qemu_tool_class):
740+
"""Test building system disk with force_disk=True in check_mode doesn't remove file"""
741+
self.mock_module.check_mode = True
742+
# System disk exists, base image doesn't exist
743+
mock_exists.side_effect = lambda path: path == '/var/lib/libvirt/images/vm.qcow2'
744+
745+
# Configure the mocked QemuImgTool
746+
mock_qemu_instance = mock.Mock()
747+
mock_qemu_tool_class.return_value = mock_qemu_instance
748+
749+
disk_param = {
750+
'path': '/var/lib/libvirt/images/vm.qcow2',
751+
'size': 20,
752+
'format': 'qcow2'
753+
}
754+
755+
result = self.operator.build_system_disk(disk_param, force_disk=True)
756+
757+
# In check_mode, file should NOT be removed
758+
mock_remove.assert_not_called()
759+
760+
# Should return disk config without creating files
761+
self.assertEqual(result, disk_param)
762+
self.mock_module.preserved_copy.assert_not_called()
763+
mock_qemu_instance.resize.assert_not_called()
764+
765+
@mock.patch('os.path.exists')
766+
def test_build_system_disk_force_disk_false_existing_fails(self, mock_exists):
767+
"""Test building system disk with force_disk=False fails when disk exists"""
768+
mock_exists.return_value = True # System disk exists
769+
770+
# Make fail_json raise an exception to simulate real behavior
771+
self.mock_module.fail_json.side_effect = Exception("Module failed")
772+
773+
disk_param = {
774+
'path': '/var/lib/libvirt/images/vm.qcow2',
775+
'size': 20
776+
}
777+
778+
with self.assertRaises(Exception):
779+
self.operator.build_system_disk(disk_param, force_disk=False)
780+
781+
self.mock_module.fail_json.assert_called_once_with(
782+
msg="The system disk file already exists: /var/lib/libvirt/images/vm.qcow2"
783+
)
784+
702785

703786
class TestBaseImageOperatorCheckMode(unittest.TestCase):
704787
"""Test BaseImageOperator check_mode support"""
@@ -928,9 +1011,13 @@ def setUp(self):
9281011
'base_image': 'https://example.com/image.qcow2',
9291012
'image_cache_dir': '/tmp/cache',
9301013
'force_pull': False,
1014+
'force_disk': False,
9311015
'disks': [{'path': '/var/lib/libvirt/images/test.qcow2', 'size': 20}],
9321016
'image_checksum': None,
933-
'url_timeout': None
1017+
'url_timeout': None,
1018+
'wait_for_cloud_init_reboot': True,
1019+
'cloud_init_auto_reboot': True,
1020+
'cloud_init_reboot_timeout': 600
9341021
}
9351022

9361023
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.BaseImageOperator')
@@ -973,7 +1060,7 @@ def test_core_vm_not_exists_create_success(self, mock_update_params, mock_valida
9731060
mock_operator.validate_checksum.assert_called_once()
9741061
mock_operator.build_system_disk.assert_called_once()
9751062
mock_update_params.assert_called_once()
976-
mock_virt_install.execute.assert_called_once_with(dryrun=False, wait_timeout=None)
1063+
mock_virt_install.execute.assert_called_once_with(dryrun=False, wait_timeout=600)
9771064

9781065
# VM should not be destroyed (doesn't exist)
9791066
mock_virt_conn.destroy.assert_not_called()
@@ -1068,7 +1155,7 @@ def test_core_vm_exists_recreate_active(self, mock_update_params, mock_validate_
10681155
mock_operator.fetch_image.assert_called_once()
10691156
mock_operator.validate_checksum.assert_called_once()
10701157
mock_operator.build_system_disk.assert_called_once()
1071-
mock_virt_install.execute.assert_called_once_with(dryrun=False, wait_timeout=None)
1158+
mock_virt_install.execute.assert_called_once_with(dryrun=False, wait_timeout=600)
10721159

10731160
# Should return success
10741161
self.assertEqual(rc, VIRT_SUCCESS)
@@ -1258,12 +1345,104 @@ def test_core_check_mode_vm_recreation(self, mock_update_params, mock_validate_d
12581345
mock_virt_conn.undefine.assert_not_called()
12591346

12601347
# But virt-install should be called with dryrun=True
1261-
mock_virt_install.execute.assert_called_once_with(dryrun=True, wait_timeout=None)
1348+
mock_virt_install.execute.assert_called_once_with(dryrun=True, wait_timeout=600)
12621349

12631350
# Should still return success
12641351
self.assertEqual(rc, VIRT_SUCCESS)
12651352
self.assertTrue(result['changed'])
12661353

1354+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.BaseImageOperator')
1355+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.VirtInstallTool')
1356+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.LibvirtWrapper')
1357+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.validate_disks')
1358+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.update_virtinst_params')
1359+
def test_core_force_disk_passed_to_build_system_disk(self, mock_update_params, mock_validate_disks,
1360+
mock_libvirt_wrapper_class, mock_virt_install_class,
1361+
mock_base_image_operator_class):
1362+
"""Test that force_disk parameter is passed to build_system_disk"""
1363+
from ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance import core, VIRT_SUCCESS
1364+
from ansible_collections.community.libvirt.plugins.module_utils.libvirt import VMNotFound
1365+
1366+
# Set force_disk to True
1367+
self.mock_module.params['force_disk'] = True
1368+
1369+
# Mock LibvirtWrapper - VM doesn't exist
1370+
mock_virt_conn = mock.Mock()
1371+
mock_virt_conn.find_vm.side_effect = VMNotFound("VM not found")
1372+
mock_libvirt_wrapper_class.return_value = mock_virt_conn
1373+
1374+
# Mock VirtInstallTool
1375+
mock_virt_install = mock.Mock()
1376+
mock_virt_install.execute.return_value = (
1377+
True, VIRT_SUCCESS, {'some': 'data'})
1378+
mock_virt_install_class.return_value = mock_virt_install
1379+
1380+
# Mock BaseImageOperator
1381+
mock_operator = mock.Mock()
1382+
mock_operator.validate_checksum.return_value = True
1383+
mock_operator.build_system_disk.return_value = {
1384+
'path': '/var/lib/libvirt/images/test.qcow2'}
1385+
mock_base_image_operator_class.return_value = mock_operator
1386+
1387+
# Execute core function
1388+
rc, result = core(self.mock_module)
1389+
1390+
# Verify force_disk=True was passed to build_system_disk
1391+
mock_operator.build_system_disk.assert_called_once_with(
1392+
{'path': '/var/lib/libvirt/images/test.qcow2', 'size': 20},
1393+
force_disk=True
1394+
)
1395+
1396+
# Should return success
1397+
self.assertEqual(rc, VIRT_SUCCESS)
1398+
self.assertTrue(result['changed'])
1399+
1400+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.BaseImageOperator')
1401+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.VirtInstallTool')
1402+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.LibvirtWrapper')
1403+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.validate_disks')
1404+
@mock.patch('ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance.update_virtinst_params')
1405+
def test_core_force_disk_false_passed_to_build_system_disk(self, mock_update_params, mock_validate_disks,
1406+
mock_libvirt_wrapper_class, mock_virt_install_class,
1407+
mock_base_image_operator_class):
1408+
"""Test that force_disk=False parameter is passed to build_system_disk"""
1409+
from ansible_collections.community.libvirt.plugins.modules.virt_cloud_instance import core, VIRT_SUCCESS
1410+
from ansible_collections.community.libvirt.plugins.module_utils.libvirt import VMNotFound
1411+
1412+
# Ensure force_disk is False (default)
1413+
self.mock_module.params['force_disk'] = False
1414+
1415+
# Mock LibvirtWrapper - VM doesn't exist
1416+
mock_virt_conn = mock.Mock()
1417+
mock_virt_conn.find_vm.side_effect = VMNotFound("VM not found")
1418+
mock_libvirt_wrapper_class.return_value = mock_virt_conn
1419+
1420+
# Mock VirtInstallTool
1421+
mock_virt_install = mock.Mock()
1422+
mock_virt_install.execute.return_value = (
1423+
True, VIRT_SUCCESS, {'some': 'data'})
1424+
mock_virt_install_class.return_value = mock_virt_install
1425+
1426+
# Mock BaseImageOperator
1427+
mock_operator = mock.Mock()
1428+
mock_operator.validate_checksum.return_value = True
1429+
mock_operator.build_system_disk.return_value = {
1430+
'path': '/var/lib/libvirt/images/test.qcow2'}
1431+
mock_base_image_operator_class.return_value = mock_operator
1432+
1433+
# Execute core function
1434+
rc, result = core(self.mock_module)
1435+
1436+
# Verify force_disk=False was passed to build_system_disk
1437+
mock_operator.build_system_disk.assert_called_once_with(
1438+
{'path': '/var/lib/libvirt/images/test.qcow2', 'size': 20},
1439+
force_disk=False
1440+
)
1441+
1442+
# Should return success
1443+
self.assertEqual(rc, VIRT_SUCCESS)
1444+
self.assertTrue(result['changed'])
1445+
12671446

12681447
if __name__ == '__main__':
12691448
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)