This commit is contained in:
2025-07-21 13:31:14 +03:00
parent 4113719334
commit 06336697a6
201 changed files with 2228 additions and 80456 deletions

View File

@@ -18,6 +18,9 @@ DefaultT = TypeVar('DefaultT')
class decort_kvmvm(DecortController):
is_vm_stopped_or_will_be_stopped: None | bool = None
guest_agent_exec_result: None | str = None
def __init__(self):
# call superclass constructor first
super(decort_kvmvm, self).__init__(AnsibleModule(**self.amodule_init_args))
@@ -31,20 +34,22 @@ class decort_kvmvm(DecortController):
# This following flag is used to avoid extra (and unnecessary) get of compute details prior to
# packaging facts before the module completes. As ""
self.skip_final_get = False
self.force_final_get = False
self.comp_id = 0
self.comp_info = None
self.acc_id = 0
self.rg_id = 0
self.aparam_image = None
validated_acc_id =0
validated_acc_id = 0
validated_rg_id = 0
validated_rg_facts = None
self.vm_to_clone_id = 0
self.vm_to_clone_info = None
if self.aparams['get_snapshot_merge_status']:
self.force_final_get = True
if arg_amodule.params['clone_from'] is not None:
self.vm_to_clone_id, self.vm_to_clone_info, _ = (
self._compute_get_by_id(
@@ -101,7 +106,7 @@ class decort_kvmvm(DecortController):
if not comp_id: # manage Compute by name -> need RG identity
if not arg_amodule.params['rg_id']: # RG ID is not set -> locate RG by name -> need account ID
validated_acc_id, _ = self.account_find(arg_amodule.params['account_name'],
validated_acc_id, self._acc_info = self.account_find(arg_amodule.params['account_name'],
arg_amodule.params['account_id'])
if not validated_acc_id:
self.result['failed'] = True
@@ -142,6 +147,7 @@ class decort_kvmvm(DecortController):
if self.comp_id:
self.comp_should_exist = True
self.acc_id = self.comp_info['accountId']
self.rg_id = self.comp_info['rgId']
self.check_amodule_args_for_change()
else:
if self.amodule.params['state'] != 'absent':
@@ -186,18 +192,82 @@ class decort_kvmvm(DecortController):
'to a DPDK network.'
)
for net in aparam_nets:
# MTU for non-DPDK networks
net_type = net['type']
if (
net['type'] != self.VMNetType.DPDK.value
and net['mtu'] is not None
net['type'] not in (
self.VMNetType.SDN.value,
self.VMNetType.EMPTY.value,
)
and not isinstance(net['id'], int)
):
check_error = True
self.message(
'Check for parameter "networks" failed:'
' MTU can be specifed only for DPDK network'
' (remove parameter "mtu" for network'
f' {net["type"]} with ID {net["id"]}).'
'Check for parameter "networks" failed: '
'Type of parameter "id" must be integer for '
f'{net["type"]} network type'
)
# MTU
net_mtu = net['mtu']
if net_mtu is not None:
mtu_net_types = (
self.VMNetType.DPDK.value,
self.VMNetType.EXTNET.value,
)
# Allowed network types for set MTU
if net_type not in mtu_net_types:
check_error = True
self.message(
'Check for parameter "networks" failed:'
' MTU can be specifed'
' only for DPDK or EXTNET network'
' (remove parameter "mtu" for network'
f' {net["type"]} with ID {net["id"]}).'
)
# Maximum MTU
MAX_MTU = 9216
if net_type in mtu_net_types and net_mtu > MAX_MTU:
check_error = True
self.message(
'Check for parameter "networks" failed:'
f' MTU must be no more than {MAX_MTU}'
' (change value for parameter "mtu" for network'
f' {net["type"]} with ID {net["id"]}).'
)
# EXTNET minimum MTU
EXTNET_MIN_MTU = 1500
if (
net_type == self.VMNetType.EXTNET.value
and net_mtu < EXTNET_MIN_MTU
):
check_error = True
self.message(
'Check for parameter "networks" failed:'
f' MTU for {self.VMNetType.EXTNET.value} network'
f' must be at least {EXTNET_MIN_MTU}'
' (change value for parameter "mtu" for network'
f' {net["type"]} with ID {net["id"]}).'
)
# DPDK minimum MTU
DPDK_MIN_MTU = 1
if (
net_type == self.VMNetType.DPDK.value
and net_mtu < DPDK_MIN_MTU
):
check_error = True
self.message(
'Check for parameter "networks" failed:'
f' MTU for {self.VMNetType.DPDK.value} network'
f' must be at least {DPDK_MIN_MTU}'
' (change value for 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:
@@ -220,7 +290,20 @@ class decort_kvmvm(DecortController):
'specified in quotes and in the format '
'"XX:XX:XX:XX:XX:XX".'
)
if self.VMNetType.SDN.value in net_types:
if not net_types.issubset(
{
self.VMNetType.SDN.value,
self.VMNetType.EMPTY.value,
self.VMNetType.VFNIC.value,
}
):
check_error = True
self.message(
'Check for parameter "networks" failed: '
'a compute can be connected to a SDN network and '
'only to VFNIC, EMPTY networks at the same time.'
)
aparam_custom_fields = self.aparams['custom_fields']
if aparam_custom_fields is not None:
if (
@@ -458,7 +541,8 @@ class decort_kvmvm(DecortController):
boot_mode=boot_mode,
boot_loader_type=loader_type,
network_interface_naming=network_interface_naming,
hot_resize=hot_resize,)
hot_resize=hot_resize,
zone_id=self.aparams['zone_id'],)
self.comp_should_exist = True
# Originally we would have had to re-read comp_info after VM was provisioned
@@ -636,6 +720,40 @@ class decort_kvmvm(DecortController):
custom_fields=aparam_custom_fields['fields'],
)
aparam_zone_id = self.aparams['zone_id']
if aparam_zone_id is not None and aparam_zone_id != self.comp_info['zoneId']:
self.compute_migrate_to_zone(
compute_id=self.comp_id,
zone_id=aparam_zone_id,
)
aparam_guest_agent = self.aparams['guest_agent']
if aparam_guest_agent is not None:
if aparam_guest_agent['enabled'] is not None:
if (
aparam_guest_agent['enabled']
and not self.comp_info['qemu_guest']['enabled']
):
self.compute_guest_agent_enable(vm_id=self.comp_id)
elif (
aparam_guest_agent['enabled'] is False
and self.comp_info['qemu_guest']['enabled']
):
self.compute_guest_agent_disable(vm_id=self.comp_id)
if aparam_guest_agent['update_available_commands']:
self.compute_guest_agent_feature_update(vm_id=self.comp_id)
aparam_guest_agent_exec = aparam_guest_agent['exec']
if aparam_guest_agent_exec is not None:
self.guest_agent_exec_result = (
self.compute_guest_agent_execute(
vm_id=self.comp_id,
cmd=aparam_guest_agent_exec['cmd'],
args=aparam_guest_agent_exec['args'],
)
)
return
@property
@@ -779,7 +897,7 @@ class decort_kvmvm(DecortController):
# If it does - save public IP address of GW VNF in ret_dict['nat_ip']
elif iface['connType'] == "VLAN": # This is direct external network connection
ret_dict['public_ips'].append(iface['ipAddress'])
ret_dict['cpu'] = self.comp_info['cpus']
ret_dict['ram'] = self.comp_info['ram']
@@ -830,6 +948,18 @@ class decort_kvmvm(DecortController):
ret_dict['affinity_rules'] = self.comp_info['affinityRules']
ret_dict['anti_affinity_rules'] = self.comp_info['antiAffinityRules']
ret_dict['zone_id'] = self.comp_info['zoneId']
ret_dict['guest_agent'] = self.comp_info['qemu_guest']
if self.guest_agent_exec_result:
ret_dict['guest_agent']['exec_result'] = self.guest_agent_exec_result # noqa: E501
if self.amodule.params['get_snapshot_merge_status']:
ret_dict['snapshot_merge_status'] = (
self.comp_info['snapshot_merge_status']
)
return ret_dict
def check_amodule_args_for_create(self):
@@ -918,6 +1048,30 @@ class decort_kvmvm(DecortController):
' to a DPDK network.'
)
if self.check_aparam_zone_id() is False:
check_errors = True
if self.aparams['guest_agent'] is not None:
check_errors = True
self.message(
'Check for parameter "guest_agent" failed: '
'guest_agent can be specified only for existing VM.'
)
if self.aparams['get_snapshot_merge_status']:
check_errors = True
self.message(
'Check for parameter "get_snapshot_merge_status" failed: '
'snapshot merge status can be retrieved only for existing VM.'
)
aparam_networks = self.aparams['networks']
if aparam_networks is not None:
net_types = {net['type'] for net in aparam_networks}
if self.VMNetType.TRUNK.value in net_types:
if self.check_aparam_networks_trunk() is False:
check_errors = True
if check_errors:
self.exit(fail=True)
@@ -1019,11 +1173,13 @@ class decort_kvmvm(DecortController):
'EXTNET',
'VFNIC',
'DPDK',
'TRUNK',
'SDN',
'EMPTY',
],
),
id=dict(
type='int',
type='raw',
),
ip_addr=dict(
type='str',
@@ -1040,6 +1196,8 @@ class decort_kvmvm(DecortController):
('type', 'EXTNET', ('id',)),
('type', 'VFNIC', ('id',)),
('type', 'DPDK', ('id',)),
('type', 'TRUNK', ('id',)),
('type', 'SDN', ('id', 'mac')),
],
),
network_order_changing=dict(
@@ -1180,6 +1338,36 @@ class decort_kvmvm(DecortController):
hot_resize=dict(
type='bool',
),
zone_id=dict(
type='int',
),
guest_agent=dict(
type='dict',
options=dict(
enabled=dict(
type='bool',
),
exec=dict(
type='dict',
options=dict(
cmd=dict(
type='str',
required=True,
),
args=dict(
type='dict',
default={},
),
),
),
update_available_commands=dict(
type='bool',
),
),
),
get_snapshot_merge_status=dict(
type='bool',
),
),
supports_check_mode=True,
required_one_of=[
@@ -1196,6 +1384,24 @@ class decort_kvmvm(DecortController):
comp_info = self.vm_to_clone_info or self.comp_info
comp_id = comp_info['id']
self.is_vm_stopped_or_will_be_stopped = (
(
comp_info['techStatus'] == 'STOPPED'
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
'halted', 'poweredoff', 'present', 'stopped',
)
)
)
or (
comp_info['techStatus'] != 'STOPPED'
and self.amodule.params['state'] in (
'halted', 'poweredoff', 'stopped',
)
)
)
aparam_boot = self.amodule.params['boot']
if aparam_boot is not None:
new_boot_disk_size = aparam_boot['disk_size']
@@ -1285,26 +1491,8 @@ class decort_kvmvm(DecortController):
'state for a blank Compute can not be "started" or "paused".'
)
is_vm_stopped_or_will_be_stopped = (
(
comp_info['techStatus'] == 'STOPPED'
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
'halted', 'poweredoff', 'present', 'stopped',
)
)
)
or (
comp_info['techStatus'] != 'STOPPED'
and self.amodule.params['state'] in (
'halted', 'poweredoff', 'stopped',
)
)
)
if self.amodule.params['rollback_to'] is not None:
if not is_vm_stopped_or_will_be_stopped:
if not self.is_vm_stopped_or_will_be_stopped:
check_errors = True
self.message(
'Check for parameter "rollback_to" failed: '
@@ -1334,7 +1522,7 @@ class decort_kvmvm(DecortController):
if (
self.aparams[param_name] is not None
and comp_info[comp_field_name] != self.aparams[param_name]
and not is_vm_stopped_or_will_be_stopped
and not self.is_vm_stopped_or_will_be_stopped
):
check_errors = True
self.message(
@@ -1343,7 +1531,7 @@ class decort_kvmvm(DecortController):
)
if self.aparams['preferred_cpu_cores'] is not None:
if not is_vm_stopped_or_will_be_stopped:
if not self.is_vm_stopped_or_will_be_stopped:
check_errors = True
self.message(
'Check for parameter "preferred_cpu_cores" failed: '
@@ -1407,7 +1595,7 @@ class decort_kvmvm(DecortController):
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
and not self.is_vm_stopped_or_will_be_stopped
):
check_errors = True
self.message(
@@ -1438,6 +1626,22 @@ class decort_kvmvm(DecortController):
'Hot resize must be enabled to change CPU or RAM.'
)
if self.check_aparam_zone_id() is False:
check_errors = True
if self.check_aparam_guest_agent() is False:
check_errors = True
if self.check_aparam_get_snapshot_merge_status() is False:
check_errors = True
aparam_networks = self.aparams['networks']
if aparam_networks is not None:
net_types = {net['type'] for net in aparam_networks}
if self.VMNetType.TRUNK.value in net_types:
if self.check_aparam_networks_trunk() is False:
check_errors = True
if check_errors:
self.exit(fail=True)
@@ -1530,6 +1734,211 @@ class decort_kvmvm(DecortController):
)
return clone_id
def check_aparam_guest_agent(self) -> bool:
check_errors = False
aparam_guest_agent = self.aparams['guest_agent']
if aparam_guest_agent:
if self.is_vm_stopped_or_will_be_stopped:
if aparam_guest_agent['update_available_commands']:
check_errors = True
self.message(
'Check for parameter '
'"guest_agent.update_available_commands" failed: '
f'VM ID {self.comp_id} must be started to update '
'available commands.'
)
is_guest_agent_enabled_or_will_be_enabled = (
(
self.comp_info['qemu_guest']['enabled']
and aparam_guest_agent['enabled'] is not False
)
or (
self.comp_info['qemu_guest']['enabled'] is False
and aparam_guest_agent['enabled']
)
)
aparam_guest_agent_exec = aparam_guest_agent['exec']
if aparam_guest_agent_exec is not None:
if self.is_vm_stopped_or_will_be_stopped:
check_errors = True
self.message(
'Check for parameter "guest_agent.exec" failed: '
f'VM ID {self.comp_id} must be started '
'to execute commands.'
)
if not is_guest_agent_enabled_or_will_be_enabled:
check_errors = True
self.message(
'Check for parameter "guest_agent.exec" failed: '
f'Guest agent for VM ID {self.comp_id} must be enabled'
' to execute commands.'
)
aparam_exec_cmd = aparam_guest_agent_exec['cmd']
available_commands = (
self.comp_info['qemu_guest']['enabled_agent_features']
)
if aparam_exec_cmd not in available_commands:
check_errors = True
self.message(
'Check for parameter "guest_agent.exec.cmd" failed: '
f'Command "{aparam_exec_cmd}" is not '
f'available for VM ID {self.comp_id}.'
)
return not check_errors
def check_aparam_get_snapshot_merge_status(self) -> bool | None:
check_errors = False
if self.aparams['get_snapshot_merge_status']:
vm_has_shared_sep_disk = False
vm_disk_ids = [disk['id'] for disk in self.comp_info['disks']]
for disk_id in vm_disk_ids:
_, disk_info = self._disk_get_by_id(disk_id=disk_id)
if disk_info['sepType'] == 'SHARED':
vm_has_shared_sep_disk = True
break
if not vm_has_shared_sep_disk:
check_errors = True
self.message(
'Check for parameter "get_snapshot_merge_status" failed: '
f'VM ID {self.comp_id} must have at least one disk with '
'SEP type SHARED to retrieve snapshot merge status.'
)
return not check_errors
def find_networks_tags_intersections(
self,
trunk_networks: list,
extnet_networks: list,
) -> bool:
has_intersections = False
def parse_trunk_tags(trunk_tags_string: str):
trunk_tags = set()
for part in trunk_tags_string.split(','):
if '-' in part:
start, end = part.split('-')
trunk_tags.update(range(int(start), int(end) + 1))
else:
trunk_tags.add(int(part))
return trunk_tags
trunk_tags_dicts = []
for trunk_network in trunk_networks:
trunk_tags_dicts.append({
'id': trunk_network['id'],
'tags_str': trunk_network['trunkTags'],
'tags': parse_trunk_tags(
trunk_tags_string=trunk_network['trunkTags']
),
'native_vlan_id': trunk_network['nativeVlanId'],
})
# find for trunk tags intersections with other networks
for i in range(len(trunk_tags_dicts)):
for j in range(i + 1, len(trunk_tags_dicts)):
intersection = (
trunk_tags_dicts[i]['tags']
& trunk_tags_dicts[j]['tags']
)
if intersection:
has_intersections = True
self.message(
'Check for parameter "networks" failed: '
f'Trunk tags {trunk_tags_dicts[i]["tags_str"]} '
f'of trunk ID {trunk_tags_dicts[i]["id"]} '
f'overlaps with trunk tags '
f'{trunk_tags_dicts[j]["tags_str"]} of trunk ID '
f'{trunk_tags_dicts[j]["id"]}'
)
for extnet in extnet_networks:
if extnet['vlanId'] in trunk_tags_dicts[i]['tags']:
has_intersections = True
self.message(
'Check for parameter "networks" failed: '
f'Trunk tags {trunk_tags_dicts[i]["tags_str"]} '
f'of trunk ID {trunk_tags_dicts[i]["id"]} '
f'overlaps with tag {extnet["vlanId"]} of extnet ID '
f'{extnet["id"]}'
)
if extnet['vlanId'] == trunk_tags_dicts[i]['native_vlan_id']:
has_intersections = True
self.message(
'Check for parameter "networks" failed: '
f'Trunk native vlan ID '
f'{trunk_tags_dicts[i]["native_vlan_id"]} of trunk ID '
f'{trunk_tags_dicts[i]["id"]} '
f'overlaps with vlan ID {extnet["vlanId"]} of extnet '
f'ID {extnet["id"]}'
)
return has_intersections
def check_aparam_networks_trunk(self) -> bool | None:
check_errors = False
# check if account has vm feature “trunk”
if not self.check_account_vm_features(vm_feature=self.VMFeature.trunk):
check_errors = True
self.message(
'Check for parameter "networks" failed: '
f'Account ID {self.acc_id} must have feature "trunk" to use '
'trunk type networks '
)
# check if rg has vm feature “trunk”
if not self.check_rg_vm_features(vm_feature=self.VMFeature.trunk):
check_errors = True
self.message(
'Check for parameter "networks" failed: '
f'RG ID {self.rg_id} must have feature "trunk" to use '
'trunk type networks '
)
aparam_trunk_networks = []
aparam_extnet_networks = []
for net in self.aparams['networks']:
if net['type'] == self.VMNetType.TRUNK.value:
aparam_trunk_networks.append(net)
elif net['type'] == self.VMNetType.EXTNET.value:
aparam_extnet_networks.append(net)
trunk_networks_info = []
# check that account has access to all specified trunks
for trunk_network in aparam_trunk_networks:
trunk_info = self.trunk_get(id=trunk_network['id'])
trunk_networks_info.append(trunk_info)
if (
trunk_info['accountIds'] is None
or self.acc_id not in trunk_info['accountIds']
):
check_errors = True
self.message(
'Check for parameter "networks" failed: '
f'Account ID {self.acc_id} does not have access to '
f'trunk ID {trunk_info['id']}'
)
extnet_networks_info = []
for extnet_network in aparam_extnet_networks:
extnet_networks_info.append(
self.extnet_get(id=extnet_network['id'])
)
# check that trunk tags do not overlap with each other
# and with extnets vlan id
if self.find_networks_tags_intersections(
trunk_networks=trunk_networks_info,
extnet_networks=extnet_networks_info,
):
check_errors = True
return not check_errors
# Workflow digest:
# 1) authenticate to DECORT controller & validate authentication by issuing API call - done when creating DECSController
# 2) check if the VM with the specified id or rg_name:name exists
@@ -1617,12 +2026,16 @@ def main():
# prepare Compute facts to be returned as part of decon.result and then call exit_json(...)
rg_facts = None
if subj.comp_should_exist:
if subj.result['changed'] and not subj.skip_final_get:
if (
(subj.result['changed'] and not subj.skip_final_get)
or subj.force_final_get
):
# There were changes to the Compute - refresh Compute facts.
_, subj.comp_info, _ = subj.compute_find(
comp_id=subj.comp_id,
need_custom_fields=True,
need_console_url=amodule.params['get_console_url'],
need_snapshot_merge_status=amodule.params['get_snapshot_merge_status'], # noqa: E501
)
#
# We no longer need to re-read RG facts, as all network info is now available inside