This commit is contained in:
2025-05-07 14:08:17 +03:00
parent f8c32d609b
commit 4113719334
36 changed files with 10638 additions and 191 deletions

View File

@@ -1,4 +1,6 @@
#!/usr/bin/python
import re
from typing import Sequence, Any, TypeVar
DOCUMENTATION = r'''
---
@@ -12,6 +14,9 @@ from ansible.module_utils.basic import env_fallback
from ansible.module_utils.decort_utils import *
DefaultT = TypeVar('DefaultT')
class decort_kvmvm(DecortController):
def __init__(self):
# call superclass constructor first
@@ -73,6 +78,8 @@ class decort_kvmvm(DecortController):
if not clone_id:
clone_id = self.clone()
if self.amodule.check_mode:
self.exit()
self.comp_id, self.comp_info, self.rg_id = self._compute_get_by_id(
comp_id=clone_id,
@@ -178,8 +185,8 @@ class decort_kvmvm(DecortController):
'hp_backed must be set to True to connect a compute '
'to a DPDK network.'
)
# MTU for non-DPDK networks
for net in aparam_nets:
# MTU for non-DPDK networks
if (
net['type'] != self.VMNetType.DPDK.value
and net['mtu'] is not None
@@ -191,6 +198,28 @@ class decort_kvmvm(DecortController):
' (remove parameter "mtu" for network'
f' {net["type"]} with ID {net["id"]}).'
)
# MAC address
if net['mac'] is not None:
if net['type'] == self.VMNetType.EMPTY.value:
check_error = True
self.message(
'Check for parameter "networks.mac" failed: '
'MAC-address cannot be specified for an '
'EMPTY type network.'
)
mac_validation_result = re.match(
'[0-9a-f]{2}([:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$',
net['mac'].lower(),
)
if not mac_validation_result:
check_error = True
self.message(
'Check for parameter "networks.mac" failed: '
f'MAC-address for network ID {net["id"]} must be '
'specified in quotes and in the format '
'"XX:XX:XX:XX:XX:XX".'
)
aparam_custom_fields = self.aparams['custom_fields']
if aparam_custom_fields is not None:
@@ -215,6 +244,22 @@ class decort_kvmvm(DecortController):
'the list must contain only unique elements.'
)
aparam_state = self.aparams['state']
new_state = None
match aparam_state:
case 'halted' | 'poweredoff':
new_state = 'stopped'
case 'poweredon':
new_state = 'started'
if new_state:
self.message(
msg=f'"{aparam_state}" state is deprecated and might be '
f'removed in newer versions. '
f'Please use "{new_state}" instead.',
warning=True,
)
if check_error:
self.exit(fail=True)
@@ -265,12 +310,40 @@ class decort_kvmvm(DecortController):
# each of the following calls will abort if argument is missing
self.check_amodule_argument('cpu')
self.check_amodule_argument('ram')
aparam_boot = self.aparams['boot']
validated_bdisk_size = 0
if self.amodule.params['boot'] is not None:
boot_mode = 'bios'
loader_type = 'unknown'
if aparam_boot is not None:
validated_bdisk_size = self.amodule.params['boot'].get(
'disk_size', 0
)
if aparam_boot['mode'] is None:
self.message(
msg=self.MESSAGES.default_value_used(
param_name='boot.mode',
default_value=boot_mode
),
warning=True,
)
else:
boot_mode = aparam_boot['mode']
if aparam_boot['loader_type'] is None:
self.message(
msg=self.MESSAGES.default_value_used(
param_name='boot.loader_type',
default_value=loader_type
),
warning=True,
)
else:
loader_type = aparam_boot['loader_type']
image_id, image_facts = None, None
if self.aparam_image:
# either image_name or image_id must be present
@@ -308,7 +381,7 @@ class decort_kvmvm(DecortController):
# Once this "feature" is fixed, make sure VM is created according to the actual desired state
#
start_compute = False # change this once a workaround for the aforementioned libvirt "feature" is implemented
if self.amodule.params['state'] in ('halted', 'poweredoff'):
if self.amodule.params['state'] in ('halted', 'poweredoff', 'stopped'):
start_compute = False
if self.amodule.params['ssh_key'] and self.amodule.params['ssh_key_user'] and not self.amodule.params['ci_user_data']:
@@ -334,7 +407,6 @@ class decort_kvmvm(DecortController):
if numa_affinity is None:
numa_affinity = 'none'
chipset = self.amodule.params['chipset']
if chipset is None:
chipset = 'i440fx'
@@ -343,6 +415,28 @@ class decort_kvmvm(DecortController):
f'default value "{chipset}" will be used.',
warning=True,
)
network_interface_naming = self.aparams['network_interface_naming']
if network_interface_naming is None:
network_interface_naming = 'ens'
self.message(
msg=self.MESSAGES.default_value_used(
param_name='network_interface_naming',
default_value=network_interface_naming
),
warning=True,
)
hot_resize = self.aparams['hot_resize']
if hot_resize is None:
hot_resize = False
self.message(
msg=self.MESSAGES.default_value_used(
param_name='hot_resize',
default_value=hot_resize
),
warning=True,
)
# if we get through here, all parameters required to create new Compute instance should be at hand
# NOTE: KVM VM is created in HALTED state and must be explicitly started
@@ -360,7 +454,11 @@ class decort_kvmvm(DecortController):
cpu_pin=cpu_pin,
hp_backed=hp_backed,
numa_affinity=numa_affinity,
preferred_cpu_cores=self.amodule.params['preferred_cpu_cores'],)
preferred_cpu_cores=self.amodule.params['preferred_cpu_cores'],
boot_mode=boot_mode,
boot_loader_type=loader_type,
network_interface_naming=network_interface_naming,
hot_resize=hot_resize,)
self.comp_should_exist = True
# Originally we would have had to re-read comp_info after VM was provisioned
@@ -407,7 +505,7 @@ class decort_kvmvm(DecortController):
label=self.amodule.params['affinity_label'],)
# NOTE: see NOTE above regarding libvirt "feature" and new VMs created in HALTED state
if self.aparam_image:
if self.amodule.params['state'] not in ('halted', 'poweredoff'):
if self.amodule.params['state'] in ('poweredon', 'started'):
self.compute_powerstate(self.comp_info, 'started')
if self.aparams['custom_fields'] is None:
@@ -553,14 +651,54 @@ class decort_kvmvm(DecortController):
'description': 'desc',
'auto_start': 'autoStart',
'preferred_cpu_cores': 'preferredCpu',
'boot.mode': 'bootType',
'boot.loader_type': 'loaderType',
'network_interface_naming': 'networkInterfaceNaming',
'hot_resize': 'hotResize',
}
def get_nested_value(
d: dict,
keys: Sequence[str],
default: DefaultT | None = None,
) -> Any | DefaultT:
if not keys:
raise ValueError
key = keys[0]
if key not in d:
return default
value = d[key]
if len(keys) > 1:
if isinstance(value, dict):
nested_d = value
return get_nested_value(
d=nested_d,
keys=keys[1:],
default=default,
)
if value is None:
return default
raise ValueError(
f'The key {key} found, but its value is not a dictionary.'
)
return value
for aparam_name, comp_field_name in params_to_check.items():
aparam_value = self.amodule.params[aparam_name]
if (
aparam_value is not None
and aparam_value != self.comp_info[comp_field_name]
):
result_args[aparam_name] = aparam_value
aparam_value = get_nested_value(
d=self.aparams,
keys=aparam_name.split('.'),
)
comp_value = get_nested_value(
d=self.comp_info,
keys=comp_field_name.split('.'),
)
if aparam_value is not None and aparam_value != comp_value:
result_args[aparam_name.replace('.', '_')] = (
aparam_value
)
return result_args
@@ -679,15 +817,49 @@ class decort_kvmvm(DecortController):
ret_dict['clones'] = self.comp_info['clones']
ret_dict['clone_reference'] = self.comp_info['cloneReference']
ret_dict['boot_mode'] = self.comp_info['bootType']
ret_dict['boot_loader_type'] = self.comp_info['loaderType']
ret_dict['network_interface_naming'] = self.comp_info[
'networkInterfaceNaming'
]
ret_dict['hot_resize'] = self.comp_info['hotResize']
ret_dict['pinned_to_stack'] = self.comp_info['pinnedToStack']
ret_dict['affinity_label'] = self.comp_info['affinityLabel']
ret_dict['affinity_rules'] = self.comp_info['affinityRules']
ret_dict['anti_affinity_rules'] = self.comp_info['antiAffinityRules']
return ret_dict
def check_amodule_args_for_create(self):
check_errors = False
# Check for unacceptable parameters for a blank Compute
if (
self.aparams['image_id'] is not None
or self.aparams['image_name'] is not None
):
self.aparam_image = True
for param in (
'network_interface_naming',
'hot_resize',
):
if self.aparams[param] is not None:
check_errors = True
self.message(
f'Check for parameter "{param}" failed: '
'parameter can be specified only for a blank VM.'
)
if self.aparams['boot'] is not None:
for param in ('mode', 'loader_type'):
if self.aparams['boot'][param] is not None:
check_errors = True
self.message(
f'Check for parameter "boot.{param}" failed: '
'parameter can be specified only for a blank VM.'
)
else:
self.aparam_image = False
if (
@@ -696,14 +868,15 @@ class decort_kvmvm(DecortController):
'present',
'poweredoff',
'halted',
'stopped',
)
):
check_errors = True
self.message(
'Check for parameter "state" failed: '
'state for a blank Compute must be either '
'"present", "poweredoff" or "halted".'
'"present" or "stopped".'
)
self.exit(fail=True)
for parameter in (
'ssh_key',
@@ -711,38 +884,41 @@ class decort_kvmvm(DecortController):
'ci_user_data',
):
if self.aparams[parameter] is not None:
check_errors = True
self.message(
f'Check for parameter "{parameter}" failed: '
f'"image_id" or "image_name" must be specified '
f'to set {parameter}.'
)
self.exit(fail=True)
if (
self.aparams['sep_id'] is not None
and self.aparams['boot'] is None
and self.aparams['boot']['disk_size'] is None
):
check_errors = True
self.message(
'Check for parameter "sep_id" failed: '
'"image_id" or "image_name" or "boot.disk_size" '
'must be specified to set sep_id.'
)
self.exit(fail=True)
if self.aparams['rollback_to'] is not None:
check_errors = True
self.message(
'Check for parameter "rollback_to" failed: '
'rollback_to can be specified only for existing compute.'
)
self.exit(fail=True)
if self.aparam_networks_has_dpdk and not self.aparams['hp_backed']:
check_errors = True
self.message(
'Check for parameter "networks" failed:'
' hp_backed must be set to True to connect a compute'
' to a DPDK network.'
)
if check_errors:
self.exit(fail=True)
@property
@@ -769,6 +945,21 @@ class decort_kvmvm(DecortController):
disk_size=dict(
type='int',
),
mode=dict(
type='str',
choices=[
'bios',
'uefi',
],
),
loader_type=dict(
type='str',
choices=[
'windows',
'linux',
'unknown',
],
),
),
),
sep_id=dict(
@@ -840,6 +1031,9 @@ class decort_kvmvm(DecortController):
mtu=dict(
type='int',
),
mac=dict(
type='str',
),
),
required_if=[
('type', 'VINS', ('id',)),
@@ -892,6 +1086,8 @@ class decort_kvmvm(DecortController):
'poweredoff',
'halted',
'poweredon',
'stopped',
'started',
'present',
],
),
@@ -974,6 +1170,16 @@ class decort_kvmvm(DecortController):
),
),
),
network_interface_naming=dict(
type='str',
choices=[
'ens',
'eth',
],
),
hot_resize=dict(
type='bool',
),
),
supports_check_mode=True,
required_one_of=[
@@ -1069,12 +1275,14 @@ class decort_kvmvm(DecortController):
if (
not comp_info['imageId']
and self.amodule.params['state'] in ('poweredon', 'paused')
and self.amodule.params['state'] in (
'poweredon', 'paused', 'started',
)
):
check_errors = True
self.message(
'Check for parameter "state" failed: '
'state for a blank Compute can not be "poweredon" or "paused".'
'state for a blank Compute can not be "started" or "paused".'
)
is_vm_stopped_or_will_be_stopped = (
@@ -1083,14 +1291,14 @@ class decort_kvmvm(DecortController):
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
'halted', 'poweredoff', 'present',
'halted', 'poweredoff', 'present', 'stopped',
)
)
)
or (
comp_info['techStatus'] != 'STOPPED'
and self.amodule.params['state'] in (
'halted', 'poweredoff',
'halted', 'poweredoff', 'stopped',
)
)
)
@@ -1120,6 +1328,7 @@ class decort_kvmvm(DecortController):
'cpu_pin': 'cpupin',
'hp_backed': 'hpBacked',
'numa_affinity': 'numaAffinity',
'hot_resize': 'hotResize',
}
for param_name, comp_field_name in params_to_check.items():
if (
@@ -1159,13 +1368,13 @@ class decort_kvmvm(DecortController):
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
'poweredon', 'present',
'poweredon', 'present', 'started',
)
)
)
or (
comp_info['techStatus'] != 'STARTED'
and self.amodule.params['state'] == 'poweredon'
and self.amodule.params['state'] in ('poweredon', 'started')
)
)
@@ -1179,30 +1388,55 @@ class decort_kvmvm(DecortController):
aparam_disks = self.aparams['disks']
if aparam_disks is not None:
if self.comp_info['snapSets']:
match aparam_disks['mode']:
case 'detach' | 'delete':
check_errors = True
self.message(
f'Check for parameter "disks" failed: '
f'cannot {aparam_disks["mode"]} disks for '
f'Compute ID {self.comp_id} while snapshots '
f'exist in compute.'
)
case 'match':
comp_disk_ids = {
disk['id'] for disk in self.comp_info['disks']
}
disks = set(aparam_disks['ids'])
disks_to_detach = comp_disk_ids - disks
if disks_to_detach:
check_errors = True
self.message(
f'Check for parameter "disks" failed: '
f'disks {disks_to_detach} cannot be detached '
f'from Compute ID {self.comp_id} while '
f'snapshots exist in compute.'
)
aparam_disks_ids = aparam_disks['ids']
comp_boot_disk_id = None
for comp_disk in self.comp_info['disks']:
if comp_disk['type'] == 'B':
comp_boot_disk_id = comp_disk['id']
break
disks_to_detach = []
match aparam_disks['mode']:
case 'detach' | 'delete':
disks_to_detach = aparam_disks_ids
case 'match':
comp_disk_ids = {
disk['id'] for disk in self.comp_info['disks']
}
disks = set(aparam_disks_ids)
disks_to_detach = comp_disk_ids - disks
if (
comp_boot_disk_id is not None
and comp_boot_disk_id in disks_to_detach
and not is_vm_stopped_or_will_be_stopped
):
check_errors = True
self.message(
f'Check for parameter "disks" failed: '
f'VM ID {comp_id} must be stopped to detach '
f'boot disk ID {comp_boot_disk_id}.'
)
if self.comp_info['snapSets'] and disks_to_detach:
check_errors = True
self.message(
f'Check for parameter "disks" failed: '
f'cannot detach disks {disks_to_detach} from '
f'Compute ID {self.comp_id} while snapshots exist.'
)
if (
(
self.aparams['cpu'] is not None
and self.aparams['cpu'] != comp_info['cpus']
) or (
self.aparams['ram'] is not None
and self.aparams['ram'] != comp_info['ram']
)
) and not (self.aparams['hot_resize'] or comp_info['hotResize']):
check_errors = True
self.message(
'Check for parameters "cpu" and "ram" failed: '
'Hot resize must be enabled to change CPU or RAM.'
)
if check_errors:
self.exit(fail=True)
@@ -1323,7 +1557,8 @@ def main():
subj.destroy()
else:
if amodule.params['state'] in (
'paused', 'poweredon', 'poweredoff', 'halted'
'paused', 'poweredon', 'poweredoff',
'halted', 'started', 'stopped',
):
subj.compute_powerstate(
comp_facts=subj.comp_info,
@@ -1331,7 +1566,7 @@ def main():
)
subj.modify(arg_wait_cycles=7)
elif subj.comp_info['status'] == "DELETED":
if amodule.params['state'] in ('present', 'poweredon'):
if amodule.params['state'] in ('present', 'poweredon', 'started'):
# TODO - check if restore API returns VM ID (similarly to VM create API)
subj.compute_restore(comp_id=subj.comp_id)
# TODO - do we need updated comp_info to manage port forwards and size after VM is restored?
@@ -1345,10 +1580,15 @@ def main():
# subj.nop()
# subj.comp_should_exist = False
subj.destroy()
elif amodule.params['state'] in ('paused', 'poweredoff', 'halted'):
elif amodule.params['state'] in (
'paused', 'poweredoff', 'halted', 'stopped'
):
subj.error()
elif subj.comp_info['status'] == "DESTROYED":
if amodule.params['state'] in ('present', 'poweredon', 'poweredoff', 'halted'):
if amodule.params['state'] in (
'present', 'poweredon', 'poweredoff',
'halted', 'started', 'stopped',
):
subj.create() # this call will also handle data disk & network connection
elif amodule.params['state'] == 'absent':
subj.nop()
@@ -1363,7 +1603,10 @@ def main():
# If requested state is 'absent' - nothing to do
if state == 'absent':
subj.nop()
elif state in ('present', 'poweredon', 'poweredoff', 'halted'):
elif state in (
'present', 'poweredon', 'poweredoff',
'halted', 'started', 'stopped',
):
subj.create() # this call will also handle data disk & network connection
elif state == 'paused':
subj.error()