You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
decort-ansible/library/decort_kvmvm.py

2493 lines
99 KiB

#!/usr/bin/python
7 months ago
import re
from typing import Sequence, Any, TypeVar
1 year ago
DOCUMENTATION = r'''
---
module: decort_kvmvm
1 year ago
description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home).
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.decort_utils import *
1 year ago
7 months ago
DefaultT = TypeVar('DefaultT')
class decort_kvmvm(DecortController):
5 months ago
is_vm_stopped_or_will_be_stopped: None | bool = None
guest_agent_exec_result: None | str = None
12 months ago
def __init__(self):
# call superclass constructor first
12 months ago
super(decort_kvmvm, self).__init__(AnsibleModule(**self.amodule_init_args))
arg_amodule = self.amodule
10 months ago
self.aparam_networks_has_dpdk = None
1 year ago
self.check_amodule_args()
self.comp_should_exist = False
# 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
5 months ago
self.force_final_get = False
self.comp_id = 0
self.comp_info = None
self.rg_id = 0
12 months ago
self.aparam_image = None
5 months ago
validated_acc_id = 0
validated_rg_id = 0
validated_rg_facts = None
9 months ago
self.vm_to_clone_id = 0
self.vm_to_clone_info = None
5 months ago
if self.aparams['get_snapshot_merge_status']:
self.force_final_get = True
9 months ago
if arg_amodule.params['clone_from'] is not None:
self.vm_to_clone_id, self.vm_to_clone_info, _ = (
self._compute_get_by_id(
comp_id=self.aparams['clone_from']['id'],
)
)
3 weeks ago
self.rg_id = self.vm_to_clone_info['rgId']
9 months ago
if not self.vm_to_clone_id:
self.message(
f'Check for parameter "clone_from.id" failed: '
f'VM ID {self.aparams["clone_from"]["id"]} does not exist.'
)
self.exit(fail=True)
elif self.vm_to_clone_info['status'] in ('DESTROYED', 'DELETED'):
self.message(
f'Check for parameter "clone_from.id" failed: '
f'VM ID {self.aparams["clone_from"]["id"]} is in '
f'{self.vm_to_clone_info["status"]} state and '
f'cannot be cloned.'
)
self.exit(fail=True)
clone_id, clone_dict, _ = self.compute_find(
comp_name=self.aparams['name'],
3 weeks ago
rg_id=self.rg_id,
9 months ago
)
self.check_amodule_args_for_clone(
clone_id=clone_id,
clone_dict=clone_dict,
)
self.check_amodule_args_for_change()
if not clone_id:
clone_id = self.clone()
7 months ago
if self.amodule.check_mode:
self.exit()
9 months ago
self.comp_id, self.comp_info, self.rg_id = self._compute_get_by_id(
comp_id=clone_id,
need_custom_fields=True,
need_console_url=self.aparams['get_console_url'],
)
return
comp_id = arg_amodule.params['id']
# Analyze Compute name & ID, RG name & ID and build arguments to compute_find accordingly.
9 months ago
if arg_amodule.params['name'] == "" and comp_id == 0:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = "Cannot manage Compute when its ID is 0 and name is empty."
self.fail_json(**self.result)
# fail the module - exit
9 months ago
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
5 months ago
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
self.result['changed'] = False
self.result['msg'] = ("Current user does not have access to the account ID {} / "
"name '{}' or non-existent account specified.").format(arg_amodule.params['account_id'],
arg_amodule.params['account_name'])
self.fail_json(**self.result)
# fail the module -> exit
# now validate RG
validated_rg_id, validated_rg_facts = self.rg_find(validated_acc_id,
arg_amodule.params['rg_id'],
arg_amodule.params['rg_name'])
if not validated_rg_id:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = "Cannot find RG ID {} / name '{}'.".format(arg_amodule.params['rg_id'],
arg_amodule.params['rg_name'])
self.fail_json(**self.result)
# fail the module - exit
self.rg_id = validated_rg_id
arg_amodule.params['rg_id'] = validated_rg_id
arg_amodule.params['rg_name'] = validated_rg_facts['name']
self.acc_id = validated_rg_facts['accountId']
# at this point we are ready to locate Compute, and if anything fails now, then it must be
# because this Compute does not exist or something goes wrong in the upstream API
# We call compute_find with check_state=False as we also consider the case when a Compute
# specified by account / RG / compute name never existed and will be created for the first time.
9 months ago
self.comp_id, self.comp_info, self.rg_id = self.compute_find(comp_id=comp_id,
comp_name=arg_amodule.params['name'],
rg_id=validated_rg_id,
12 months ago
check_state=False,
9 months ago
need_custom_fields=True,
need_console_url=self.aparams['get_console_url'])
if self.comp_id:
self.comp_should_exist = True
self.acc_id = self.comp_info['accountId']
5 months ago
self.rg_id = self.comp_info['rgId']
9 months ago
self.check_amodule_args_for_change()
1 year ago
else:
10 months ago
if self.amodule.params['state'] != 'absent':
self.check_amodule_args_for_create()
9 months ago
return
1 year ago
def check_amodule_args(self):
"""
Additional Ansible Module arguments validation that
cannot be implemented using Ansible Argument spec.
"""
10 months ago
check_error = False
12 months ago
# Check parameter "networks"
1 year ago
aparam_nets = self.aparams['networks']
if aparam_nets:
net_types = {net['type'] for net in aparam_nets}
12 months ago
# DPDK and other networks
10 months ago
self.aparam_networks_has_dpdk = False
12 months ago
if self.VMNetType.DPDK.value in net_types:
10 months ago
self.aparam_networks_has_dpdk = True
12 months ago
if not net_types.issubset(
{self.VMNetType.DPDK.value, self.VMNetType.EMPTY.value}
):
check_error = True
self.message(
'Check for parameter "networks" failed:'
' a compute cannot be connected to a DPDK network and'
' a network of another type at the same time.'
)
10 months ago
if (
self.aparams['hp_backed'] is not None
and not self.aparams['hp_backed']
):
check_error = True
self.message(
'Check for parameter "networks" failed: '
'hp_backed must be set to True to connect a compute '
'to a DPDK network.'
)
12 months ago
for net in aparam_nets:
5 months ago
net_type = net['type']
12 months ago
if (
5 months ago
net['type'] not in (
self.VMNetType.SDN.value,
self.VMNetType.EMPTY.value,
)
and not isinstance(net['id'], int)
12 months ago
):
check_error = True
self.message(
5 months ago
'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,
12 months ago
)
5 months ago
# 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"]}).'
)
7 months ago
# 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".'
)
5 months ago
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.'
)
1 year ago
aparam_custom_fields = self.aparams['custom_fields']
if aparam_custom_fields is not None:
1 year ago
if (
1 year ago
aparam_custom_fields['disable']
and aparam_custom_fields['fields'] is not None
1 year ago
):
10 months ago
check_error = True
1 year ago
self.message(
1 year ago
'Check for parameter "custom_fields" failed: '
'"fields" cannot be set if "disable" is True.'
1 year ago
)
10 months ago
aparam_pref_cpu_cores = self.aparams['preferred_cpu_cores']
if (
aparam_pref_cpu_cores
and len(set(aparam_pref_cpu_cores)) != len(aparam_pref_cpu_cores)
):
check_error = True
self.message(
'Check for parameter "preferred_cpu_cores" failed: '
'the list must contain only unique elements.'
)
7 months ago
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,
)
10 months ago
if check_error:
self.exit(fail=True)
1 year ago
def nop(self):
"""No operation (NOP) handler for Compute management by decort_kvmvm module.
This function is intended to be called from the main switch construct of the module
when current state -> desired state change logic does not require any changes to
the actual Compute state.
"""
self.result['failed'] = False
self.result['changed'] = False
if self.comp_id:
self.result['msg'] = ("No state change required for Compute ID {} because of its "
"current status '{}'.").format(self.comp_id, self.comp_info['status'])
else:
self.result['msg'] = ("No state change to '{}' can be done for "
"non-existent Compute instance.").format(self.amodule.params['state'])
return
def error(self):
"""Error handler for Compute instance management by decort_kvmvm module.
This function is intended to be called when an invalid state change is requested.
Invalid means that the current is invalid for any operations on the Compute or the
transition from current to desired state is not technically possible.
"""
self.result['failed'] = True
self.result['changed'] = False
if self.comp_id:
self.result['msg'] = ("Invalid target state '{}' requested for Compute ID {} in the "
"current status '{}'.").format(self.comp_id,
self.amodule.params['state'],
self.comp_info['status'])
else:
self.result['msg'] = ("Invalid target state '{}' requested for non-existent Compute name '{}' "
"in RG ID {} / name '{}'").format(self.amodule.params['state'],
self.amodule.params['name'],
self.amodule.params['rg_id'],
self.amodule.params['rg_name'])
return
def create(self):
"""New Compute instance creation handler for decort_kvmvm module.
This function checks for the presence of required parameters and deploys a new KVM VM
Compute instance with the specified characteristics into the target Resource Group.
The target RG must exist.
"""
3 weeks ago
# the following parameters must be present: cpu, ram, image_id
# each of the following calls will abort if argument is missing
self.check_amodule_argument('cpu')
self.check_amodule_argument('ram')
7 months ago
aparam_boot = self.aparams['boot']
9 months ago
validated_bdisk_size = 0
7 months ago
boot_mode = 'bios'
loader_type = 'unknown'
if aparam_boot is not None:
9 months ago
validated_bdisk_size = self.amodule.params['boot'].get(
'disk_size', 0
)
1 year ago
7 months ago
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']
1 year ago
image_id, image_facts = None, None
12 months ago
if self.aparam_image:
1 year ago
if (
self.check_amodule_argument('image_id', abort=False)
and self.amodule.params['image_id'] > 0
):
# find image by image ID and account ID
3 weeks ago
# image_find(self, image_id, account_id, rg_id=0, sepid=0, pool=""):
1 year ago
image_id, image_facts = self.image_find(
image_id=self.amodule.params['image_id'],
account_id=self.acc_id)
if validated_bdisk_size <= image_facts['size']:
# adjust disk size to the minimum allowed by OS image, which will be used to spin off this Compute
validated_bdisk_size = image_facts['size']
# NOTE: due to a libvirt "feature", that impacts management of a VM created without any network interfaces,
# we create KVM VM in HALTED state.
# Consequently, if desired state is different from 'halted' or 'porewedoff", we should explicitly start it
# in the upstream code.
# See corresponding NOTE below for another place where this "feature" is redressed for.
#
# Once this "feature" is fixed, make sure VM is created according to the actual desired state
#
12 months ago
start_compute = False # change this once a workaround for the aforementioned libvirt "feature" is implemented
7 months ago
if self.amodule.params['state'] in ('halted', 'poweredoff', 'stopped'):
start_compute = False
4 years ago
if self.amodule.params['ssh_key'] and self.amodule.params['ssh_key_user'] and not self.amodule.params['ci_user_data']:
cloud_init_params = {'users': [
{"name": self.amodule.params['ssh_key_user'],
"ssh-authorized-keys": [self.amodule.params['ssh_key']],
"shell": '/bin/bash'}
]}
4 years ago
elif self.amodule.params['ci_user_data']:
cloud_init_params = self.amodule.params['ci_user_data']
else:
cloud_init_params = None
1 year ago
cpu_pin = self.aparams['cpu_pin']
if cpu_pin is None:
cpu_pin = False
hp_backed = self.aparams['hp_backed']
if hp_backed is None:
hp_backed = False
numa_affinity = self.aparams['numa_affinity']
if numa_affinity is None:
numa_affinity = 'none'
10 months ago
chipset = self.amodule.params['chipset']
if chipset is None:
chipset = 'i440fx'
self.message(
msg=f'Chipset not specified, '
f'default value "{chipset}" will be used.',
warning=True,
)
7 months ago
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,
)
1 year ago
# if we get through here, all parameters required to create new Compute instance should be at hand
1 year ago
# NOTE: KVM VM is created in HALTED state and must be explicitly started
3 weeks ago
self.comp_id = self.kvmvm_provision(
rg_id=self.rg_id,
comp_name=self.amodule.params['name'],
cpu=self.amodule.params['cpu'],
ram=self.amodule.params['ram'],
boot_disk_size=validated_bdisk_size,
image_id=image_id,
description=self.amodule.params['description'],
userdata=cloud_init_params,
sep_id=self.amodule.params['sep_id' ] if "sep_id" in self.amodule.params else None,
pool_name=self.amodule.params['pool'] if "pool" in self.amodule.params else None,
start_on_create=start_compute,
chipset=chipset,
cpu_pin=cpu_pin,
hp_backed=hp_backed,
numa_affinity=numa_affinity,
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,
zone_id=self.aparams['zone_id'],
storage_policy_id=self.aparams['storage_policy_id'],
os_version=self.aparams['os_version'],
)
self.comp_should_exist = True
# Originally we would have had to re-read comp_info after VM was provisioned
# _, self.comp_info, _ = self.compute_find(self.comp_id)
# However, to avoid extra call to compute/get API we need to construct comp_info so that
# the below calls to compute_networks and compute_data_disks work properly.
#
# Here we are imitating comp_info structure as if it has been returned by a real call
# to API compute/get
self.comp_info = {
'id': self.comp_id,
'accountId': self.acc_id,
'status': "ENABLED",
'techStatus': "STOPPED",
'interfaces': [], # new compute instance is created network-less
'disks': [], # new compute instance is created without any data disks attached
'tags': {},
'affinityLabel': "",
'affinityRules': [],
'antiAffinityRules': [],
}
#
# Compute was created
#
# Setup network connections
1 year ago
if self.amodule.params['networks'] is not None:
self.compute_networks(
comp_dict=self.comp_info,
new_networks=self.amodule.params['networks'],
)
# Next manage data disks
9 months ago
if self.amodule.params['disks'] is not None:
self.compute_disks(
1 year ago
comp_dict=self.comp_info,
3 weeks ago
aparam_disks_dict=self.amodule.params['disks'],
1 year ago
)
4 years ago
self.compute_affinity(self.comp_info,
self.amodule.params['tag'],
self.amodule.params['aff_rule'],
self.amodule.params['aaff_rule'],
label=self.amodule.params['affinity_label'],)
# NOTE: see NOTE above regarding libvirt "feature" and new VMs created in HALTED state
12 months ago
if self.aparam_image:
7 months ago
if self.amodule.params['state'] in ('poweredon', 'started'):
12 months ago
self.compute_powerstate(self.comp_info, 'started')
12 months ago
if self.aparams['custom_fields'] is None:
custom_fields_disable = True
custom_fields_fields = None
else:
custom_fields_disable = self.aparams['custom_fields']['disable']
custom_fields_fields = self.aparams['custom_fields']['fields']
if not custom_fields_disable:
self.compute_set_custom_fields(
compute_id=self.comp_info['id'],
custom_fields=custom_fields_fields,
)
# read in Compute facts once more after all initial setup is complete
12 months ago
_, self.comp_info, _ = self.compute_find(
comp_id=self.comp_id,
need_custom_fields=True,
9 months ago
need_console_url=self.amodule.params['get_console_url'],
12 months ago
)
12 months ago
if self.compute_update_args:
self.compute_update(
compute_id=self.comp_info['id'],
**self.compute_update_args,
)
else:
self.skip_final_get = True
return
def destroy(self):
"""Compute destroy handler for VM management by decort_kvmvm module.
Note that this handler deletes the VM permanently together with all assigned disk resources.
"""
self.compute_delete(comp_id=self.comp_id, permanently=True)
3 weeks ago
self.comp_id, self.comp_info, _ = self._compute_get_by_id(self.comp_id)
return
def restore(self):
"""Compute restore handler for Compute instance management by decort_kvmvm module.
Note that restoring Compute is only possible if it is in DELETED state. If called on a
Compute instance in any other state, the method will throw an error and abort the execution
of the module.
"""
self.compute_restore(comp_id=self.comp_id)
# TODO - do we need updated comp_info to manage port forwards and size after VM is restored?
12 months ago
_, self.comp_info, _ = self.compute_find(
comp_id=self.comp_id,
need_custom_fields=True,
9 months ago
need_console_url=self.amodule.params['get_console_url'],
12 months ago
)
self.modify()
self.comp_should_exist = True
return
def modify(self, arg_wait_cycles=0):
"""Compute modify handler for KVM VM management by decort_kvmvm module.
This method is a convenience wrapper that calls individual Compute modification functions from
DECORT utility library (module).
Note that it does not modify power state of KVM VM.
"""
10 months ago
if self.compute_update_args:
self.compute_update(
compute_id=self.comp_info['id'],
**self.compute_update_args,
)
if self.amodule.params['rollback_to'] is not None:
self.compute_rollback(
compute_id=self.comp_info['id'],
snapshot_label=self.amodule.params['rollback_to'],
)
1 year ago
if self.amodule.params['networks'] is not None:
self.compute_networks(
comp_dict=self.comp_info,
new_networks=self.aparams['networks'],
order_changing=self.aparams['network_order_changing'],
)
9 months ago
if self.amodule.params['disks'] is not None:
self.compute_disks(
comp_dict=self.comp_info,
3 weeks ago
aparam_disks_dict=self.amodule.params['disks'],
9 months ago
)
1 year ago
9 months ago
aparam_boot = self.amodule.params['boot']
if aparam_boot is not None:
aparam_disk_id = aparam_boot['disk_id']
if aparam_disk_id is not None:
for disk in self.comp_info['disks']:
if disk['id'] == aparam_disk_id and disk['type'] != 'B':
self.compute_boot_disk(
comp_id=self.comp_info['id'],
boot_disk=aparam_disk_id,
)
break
boot_disk_new_size = aparam_boot['disk_size']
if boot_disk_new_size:
self.compute_bootdisk_size(self.comp_info, boot_disk_new_size)
1 year ago
3 weeks ago
boot_order = aparam_boot['order']
if (
boot_order is not None
and self.comp_info['bootOrder'] != boot_order
):
self.compute_set_boot_order(
vm_id=self.comp_id,
order=boot_order,
)
disk_redeploy = aparam_boot['disk_redeploy']
if disk_redeploy:
auto_start = False
if self.aparams['state'] is None:
if self.comp_info['techStatus'] == 'STARTED':
auto_start = True
else:
if self.aparams['state'] == 'started':
auto_start = True
disk_size = None
if (
aparam_boot is not None
and aparam_boot['disk_size'] is not None
):
disk_size = aparam_boot['disk_size']
elif self.aparams['image_id'] is not None:
_, image_facts = self.image_find(
image_id=self.aparams['image_id'],
)
disk_size = image_facts['size']
os_version = None
if (
self.aparams['image_id'] is None
or self.aparams['image_id'] == self.comp_info['imageId']
):
if self.aparams['os_version'] is None:
os_version = self.comp_info['os_version']
else:
os_version = self.aparams['os_version']
elif self.aparams['image_id'] != self.comp_info['imageId']:
os_version = self.aparams['os_version']
self.compute_disk_redeploy(
vm_id=self.comp_id,
storage_policy_id=self.aparams['storage_policy_id'],
image_id=self.aparams['image_id'],
disk_size=disk_size,
auto_start=auto_start,
os_version=os_version,
)
self.compute_resize(self.comp_info,
self.amodule.params['cpu'], self.amodule.params['ram'],
wait_for_state_change=arg_wait_cycles)
1 year ago
4 years ago
self.compute_affinity(self.comp_info,
self.amodule.params['tag'],
self.amodule.params['aff_rule'],
self.amodule.params['aaff_rule'],
label=self.amodule.params['affinity_label'])
1 year ago
aparam_custom_fields = self.amodule.params['custom_fields']
if aparam_custom_fields is not None:
12 months ago
compute_custom_fields = self.comp_info['custom_fields']
1 year ago
if aparam_custom_fields['disable']:
if compute_custom_fields is not None:
self.compute_disable_custom_fields(
compute_id=self.comp_info['id'],
)
else:
if compute_custom_fields != aparam_custom_fields['fields']:
self.compute_set_custom_fields(
compute_id=self.comp_info['id'],
custom_fields=aparam_custom_fields['fields'],
)
5 months ago
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'],
)
)
3 weeks ago
aparam_cdrom = self.aparams['cdrom']
if aparam_cdrom is not None:
mode = aparam_cdrom['mode']
image_id = aparam_cdrom['image_id']
if (
mode == 'insert'
and self.comp_info['cdImageId'] != image_id
):
self.compute_cd_insert(
vm_id=self.comp_id,
image_id=image_id,
)
elif mode == 'eject':
self.compute_cd_eject(
vm_id=self.comp_id,
)
if self.aparams['abort_cloning']:
self.compute_clone_abort(
vm_id=self.comp_id,
)
return
1 year ago
@property
def compute_update_args(self) -> dict:
result_args = {}
1 year ago
params_to_check = {
'name': 'name',
'chipset': 'chipset',
'cpu_pin': 'cpupin',
'hp_backed': 'hpBacked',
'numa_affinity': 'numaAffinity',
'description': 'desc',
12 months ago
'auto_start': 'autoStart',
10 months ago
'preferred_cpu_cores': 'preferredCpu',
7 months ago
'boot.mode': 'bootType',
'boot.loader_type': 'loaderType',
'network_interface_naming': 'networkInterfaceNaming',
'hot_resize': 'hotResize',
3 weeks ago
'os_version': 'os_version',
1 year ago
}
7 months ago
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
10 months ago
for aparam_name, comp_field_name in params_to_check.items():
7 months ago
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:
3 weeks ago
# If disk_redeploy = True no need to update os_version.
# Updating os_version through compute_disk_redeploy
if (
aparam_name == 'os_version'
and self.aparams['boot'] is not None
and self.aparams['boot']['disk_redeploy']
):
continue
7 months ago
result_args[aparam_name.replace('.', '_')] = (
aparam_value
)
1 year ago
return result_args
def package_facts(self, check_mode=False):
"""Package a dictionary of KVM VM facts according to the decort_kvmvm module specification.
This dictionary will be returned to the upstream Ansible engine at the completion of decort_kvmvm
module run.
@param check_mode: boolean that tells if this Ansible module is run in check mode
@return: dictionary of KVM VM facts, containing suffucient information to manage the KVM VM in
subsequent Ansible tasks.
"""
ret_dict = dict(id=0,
name="",
arch="",
cpu="",
ram="",
disk_size=0,
state="CHECK_MODE",
tech_status="",
account_id=0,
rg_id=0,
username="",
password="",
public_ips=[], # direct IPs; this list can be empty
private_ips=[], # IPs on ViNSes; usually, at least one IP is listed
nat_ip="", # IP of the external ViNS interface; can be empty.
tags={},
1 year ago
chipset="",
interfaces=[],
1 year ago
cpu_pin="",
hp_backed="",
numa_affinity="",
custom_fields={},
12 months ago
vnc_password="",
10 months ago
snapshots=[],
preferred_cpu_cores=[],
9 months ago
clones=[],
clone_reference=0,
)
if check_mode or self.comp_info is None:
# if in check mode (or void facts provided) return immediately with the default values
return ret_dict
# if not self.comp_should_exist:
# ret_dict['state'] = "ABSENT"
# return ret_dict
ret_dict['id'] = self.comp_info['id']
ret_dict['name'] = self.comp_info['name']
ret_dict['arch'] = self.comp_info['arch']
ret_dict['state'] = self.comp_info['status']
ret_dict['tech_status'] = self.comp_info['techStatus']
ret_dict['account_id'] = self.comp_info['accountId']
ret_dict['rg_id'] = self.comp_info['rgId']
if self.comp_info['tags']:
ret_dict['tags'] = self.comp_info['tags']
# if the VM is an imported VM, then the 'accounts' list may be empty,
# so check for this case before trying to access login and passowrd values
if len(self.comp_info['osUsers']):
ret_dict['username'] = self.comp_info['osUsers'][0]['login']
ret_dict['password'] = self.comp_info['osUsers'][0]['password']
if self.comp_info['interfaces']:
# We need a list of all ViNSes in the account, which owns this Compute
# to find a ViNS, which may have active external connection. Then
# we will save external IP address of that connection in ret_dict['nat_ip']
for iface in self.comp_info['interfaces']:
if iface['connType'] == "VXLAN": # This is ViNS connection
ret_dict['private_ips'].append(iface['ipAddress'])
# if iface['connId']
# Now we need to check if this ViNS has GW function and external connection.
# 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'])
5 months ago
3 weeks ago
iface['security_group_mode'] = iface.pop('enable_secgroups')
iface['security_group_ids'] = iface.pop('security_groups')
ret_dict['cpu'] = self.comp_info['cpus']
ret_dict['ram'] = self.comp_info['ram']
ret_dict['image_id'] = self.comp_info['imageId']
3 weeks ago
ret_dict['disks'] = self.comp_info['disks']
for disk in ret_dict['disks']:
if disk['type'] == 'B':
# if it is a boot disk - store its size
3 weeks ago
ret_dict['disk_size'] = disk['sizeMax']
1 year ago
ret_dict['chipset'] = self.comp_info['chipset']
ret_dict['interfaces'] = self.comp_info['interfaces']
1 year ago
ret_dict['cpu_pin'] = self.comp_info['cpupin']
ret_dict['hp_backed'] = self.comp_info['hpBacked']
ret_dict['numa_affinity'] = self.comp_info['numaAffinity']
12 months ago
ret_dict['custom_fields'] = self.comp_info['custom_fields']
1 year ago
12 months ago
ret_dict['vnc_password'] = self.comp_info['vncPasswd']
ret_dict['auto_start'] = self.comp_info['autoStart']
10 months ago
ret_dict['snapshots'] = self.comp_info['snapSets']
ret_dict['preferred_cpu_cores'] = self.comp_info['preferredCpu']
9 months ago
if self.amodule.params['get_console_url']:
ret_dict['console_url'] = self.comp_info['console_url']
ret_dict['clones'] = self.comp_info['clones']
ret_dict['clone_reference'] = self.comp_info['cloneReference']
7 months ago
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']
5 months ago
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']
)
3 weeks ago
ret_dict['cd_image_id'] = self.comp_info['cdImageId']
ret_dict['boot_order'] = self.comp_info['bootOrder']
ret_dict['os_version'] = self.comp_info['os_version']
ret_dict['boot_loader_metaiso'] = self.comp_info['loaderMetaIso']
if self.comp_info['loaderMetaIso'] is not None:
ret_dict['boot_loader_metaiso'] = {
'device_name': self.comp_info['loaderMetaIso']['devicename'],
'path': self.comp_info['loaderMetaIso']['path'],
}
if self.amodule.params['get_cloning_status']:
ret_dict['cloning_status'] = self.compute_get_clone_status(
vm_id=self.comp_id,
)
return ret_dict
1 year ago
def check_amodule_args_for_create(self):
7 months ago
check_errors = False
1 year ago
# Check for unacceptable parameters for a blank Compute
3 weeks ago
if self.aparams['image_id'] is not None:
12 months ago
self.aparam_image = True
7 months ago
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.'
)
12 months ago
else:
self.aparam_image = False
if (
self.aparams['state'] is not None
and self.aparams['state'] not in (
'present',
'poweredoff',
'halted',
7 months ago
'stopped',
12 months ago
)
):
7 months ago
check_errors = True
12 months ago
self.message(
'Check for parameter "state" failed: '
'state for a blank Compute must be either '
7 months ago
'"present" or "stopped".'
12 months ago
)
1 year ago
for parameter in (
'ssh_key',
'ssh_key_user',
'ci_user_data',
):
if self.aparams[parameter] is not None:
7 months ago
check_errors = True
1 year ago
self.message(
f'Check for parameter "{parameter}" failed: '
3 weeks ago
f'"image_id" must be specified '
1 year ago
f'to set {parameter}.'
)
if (
self.aparams['sep_id'] is not None
9 months ago
and self.aparams['boot'] is None
and self.aparams['boot']['disk_size'] is None
1 year ago
):
7 months ago
check_errors = True
1 year ago
self.message(
'Check for parameter "sep_id" failed: '
3 weeks ago
'"image_id" or "boot.disk_size" '
1 year ago
'must be specified to set sep_id.'
)
10 months ago
if self.aparams['rollback_to'] is not None:
7 months ago
check_errors = True
10 months ago
self.message(
'Check for parameter "rollback_to" failed: '
'rollback_to can be specified only for existing compute.'
)
if self.aparam_networks_has_dpdk and not self.aparams['hp_backed']:
7 months ago
check_errors = True
10 months ago
self.message(
'Check for parameter "networks" failed:'
' hp_backed must be set to True to connect a compute'
' to a DPDK network.'
)
7 months ago
5 months ago
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
3 weeks ago
if self.aparams['cdrom'] is not None:
check_errors = True
self.message(
'Check for parameter "cdrom" failed: '
'cdrom can be specified only for existing compute.'
)
aparam_storage_policy_id = self.aparams['storage_policy_id']
if aparam_storage_policy_id is None:
check_errors = True
self.message(
msg='Check for parameter "storage_policy_id" failed: '
'storage_policy_id must be specified when creating '
'a new compute'
)
elif (
aparam_storage_policy_id
not in self.rg_info['storage_policy_ids']
):
check_errors = True
self.message(
msg='Check for parameter "storage_policy_id" failed: '
f'RG ID {self.rg_id} does not have access to '
f'storage_policy_id {aparam_storage_policy_id}'
)
if self.aparams['abort_cloning'] is not None:
check_errors = True
self.message(
'Check for parameter "abort_cloning" failed: '
'abort_cloning can be specified only for existing compute.'
)
7 months ago
if check_errors:
10 months ago
self.exit(fail=True)
12 months ago
@property
def amodule_init_args(self) -> dict:
return self.pack_amodule_init_args(
argument_spec=dict(
account_id=dict(
type='int',
default=0,
1 year ago
),
12 months ago
account_name=dict(
type='str',
default='',
),
description=dict(
type='str',
),
9 months ago
boot=dict(
type='dict',
options=dict(
disk_id=dict(
type='int',
),
disk_size=dict(
type='int',
),
7 months ago
mode=dict(
type='str',
choices=[
'bios',
'uefi',
],
),
loader_type=dict(
type='str',
choices=[
'windows',
'linux',
'unknown',
],
),
3 weeks ago
from_cdrom=dict(
type='int',
),
order=dict(
type='list',
elements='str',
choices=[
e.value for e in self.VMBootDevice
],
),
disk_redeploy=dict(
type='bool',
),
9 months ago
),
12 months ago
),
sep_id=dict(
type='int',
),
pool=dict(
type='str',
),
controller_url=dict(
type='str',
required=True,
),
cpu=dict(
type='int',
),
9 months ago
disks=dict(
type='dict',
options=dict(
mode=dict(
type='str',
choices=[
'update',
'detach',
'delete',
'match',
],
default='update',
),
3 weeks ago
objects=dict(
9 months ago
type='list',
3 weeks ago
elements='dict',
options=dict(
id=dict(
type='int',
required=True,
),
pci_slot_num_hex=dict(
type='str',
),
bus_num_hex=dict(
type='str',
),
),
required_together=[
('pci_slot_num_hex', 'bus_num_hex'),
],
9 months ago
),
),
12 months ago
),
id=dict(
type='int',
default=0,
),
image_id=dict(
type='int',
),
name=dict(
type='str',
),
networks=dict(
type='list',
elements='dict',
options=dict(
type=dict(
type='str',
required=True,
choices=[
'VINS',
'EXTNET',
'VFNIC',
'DPDK',
5 months ago
'TRUNK',
'SDN',
12 months ago
'EMPTY',
],
),
id=dict(
5 months ago
type='raw',
12 months ago
),
ip_addr=dict(
type='str',
),
mtu=dict(
type='int',
),
7 months ago
mac=dict(
type='str',
),
3 weeks ago
security_group_ids=dict(
type='list',
elements='int',
),
security_group_mode=dict(
type='bool',
),
enabled=dict(
type='bool',
),
1 year ago
),
12 months ago
required_if=[
('type', 'VINS', ('id',)),
('type', 'EXTNET', ('id',)),
('type', 'VFNIC', ('id',)),
('type', 'DPDK', ('id',)),
5 months ago
('type', 'TRUNK', ('id',)),
3 weeks ago
('type', 'SDN', ('id',)),
12 months ago
],
),
network_order_changing=dict(
type='bool',
default=False,
),
ram=dict(
type='int',
),
rg_id=dict(
type='int',
default=0,
),
rg_name=dict(
type='str',
default='',
),
ssh_key=dict(
type='str',
),
ssh_key_user=dict(
type='str',
),
tag=dict(
type='dict',
),
affinity_label=dict(
type='str',
),
aff_rule=dict(
type='list',
),
aaff_rule=dict(
type='list',
),
ci_user_data=dict(
type='dict',
),
state=dict(
type='str',
choices=[
'absent',
'paused',
'poweredoff',
'halted',
'poweredon',
7 months ago
'stopped',
'started',
12 months ago
'present',
],
),
tags=dict(
type='str',
),
chipset=dict(
type='str',
choices=[
'Q35',
'i440fx',
]
),
cpu_pin=dict(
type='bool',
),
hp_backed=dict(
type='bool',
),
numa_affinity=dict(
type='str',
choices=[
'strict',
'loose',
'none',
],
),
custom_fields=dict(
type='dict',
options=dict(
fields=dict(
type='dict',
),
disable=dict(
type='bool',
),
1 year ago
),
),
12 months ago
auto_start=dict(
type='bool',
10 months ago
),
rollback_to=dict(
type='str',
),
preferred_cpu_cores=dict(
type='list',
elements='int',
),
9 months ago
get_console_url=dict(
type='bool',
default=False,
),
clone_from=dict(
type='dict',
options=dict(
id=dict(
type='int',
required=True,
),
force=dict(
type='bool',
default=False,
),
snapshot=dict(
type='dict',
options=dict(
name=dict(
type='str',
),
timestamp=dict(
type='int',
),
datetime=dict(
type='str',
),
),
mutually_exclusive=[
('name', 'timestamp', 'datetime'),
],
),
3 weeks ago
sep_pool_name=dict(
type='str',
),
sep_id=dict(
type='int',
),
storage_policy_id=dict(
type='int',
requiered=True,
),
9 months ago
),
),
7 months ago
network_interface_naming=dict(
type='str',
choices=[
'ens',
'eth',
],
),
hot_resize=dict(
type='bool',
),
5 months ago
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',
),
3 weeks ago
cdrom=dict(
type='dict',
options=dict(
mode=dict(
type='str',
choices=[
'insert',
'eject',
],
default='insert',
),
image_id=dict(
type='int',
),
),
),
storage_policy_id=dict(
type='int',
),
os_version=dict(
type='str',
),
get_cloning_status=dict(
type='bool',
),
abort_cloning=dict(
type='bool',
),
1 year ago
),
12 months ago
supports_check_mode=True,
required_one_of=[
('id', 'name'),
],
9 months ago
required_by={
'clone_from': 'name',
},
)
3 weeks ago
1 year ago
def check_amodule_args_for_change(self):
10 months ago
check_errors = False
9 months ago
comp_info = self.vm_to_clone_info or self.comp_info
comp_id = comp_info['id']
5 months ago
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',
)
)
)
9 months ago
aparam_boot = self.amodule.params['boot']
if aparam_boot is not None:
aparam_disks = self.amodule.params['disks']
aparam_boot_disk_id = aparam_boot['disk_id']
comp_disk_ids = [disk['id'] for disk in self.comp_info['disks']]
if aparam_disks is None:
if (
aparam_boot_disk_id is not None
and aparam_boot_disk_id not in comp_disk_ids
):
check_errors = True
self.message(
f'Check for parameter "boot.disk_id" failed: '
f'disk {aparam_boot_disk_id} is not attached to '
f'Compute ID {self.comp_id}.'
)
else:
match aparam_disks['mode']:
case 'update':
if (
aparam_boot_disk_id not in comp_disk_ids
and aparam_boot_disk_id not in aparam_disks['ids']
):
check_errors = True
self.message(
f'Check for parameter "boot.disk_id" failed: '
f'disk {aparam_boot_disk_id} is not attached '
f'to Compute ID {self.comp_id}.'
)
case 'match':
if aparam_boot_disk_id not in aparam_disks['ids']:
check_errors = True
self.message(
f'Check for parameter "boot.disk_id" failed: '
f'disk {aparam_boot_disk_id} is not in '
f'disks.ids'
)
case 'detach' | 'delete':
if aparam_boot_disk_id in aparam_disks['ids']:
check_errors = True
self.message(
f'Check for parameter "boot.disk_id" failed: '
f'disk {aparam_boot_disk_id} cannot be '
f'detached or deleted to set as boot disk.'
)
elif aparam_boot_disk_id not in comp_disk_ids:
check_errors = True
self.message(
f'Check for parameter "boot.disk_id" failed: '
f'disk {aparam_boot_disk_id} is not attached '
f'to Compute ID {self.comp_id}.'
)
3 weeks ago
if self.check_aparam_boot_disk_redeploy() is False:
check_errors = True
new_boot_disk_size = aparam_boot['disk_size']
if new_boot_disk_size is not None:
boot_disk_size = 0
for disk in self.comp_info['disks']:
if disk['type'] == 'B':
boot_disk_size = disk['sizeMax']
break
else:
if aparam_boot is None or aparam_boot['disk_id'] is None:
check_errors = True
self.message(
f'Can\'t set boot disk size for Compute '
f'{comp_id}, because it doesn\'t '
f'have a boot disk.'
)
if new_boot_disk_size < boot_disk_size:
check_errors = True
self.message(
f'New boot disk size {new_boot_disk_size} is less'
f' than current {boot_disk_size} for Compute ID '
f'{comp_id}'
)
cd_rom_image_id = aparam_boot['from_cdrom']
if cd_rom_image_id is not None:
if not (
self.comp_info['techStatus'] == 'STOPPED'
and self.aparams['state'] == 'started'
):
check_errors = True
self.message(
f'Check for parameter "boot.from_cdrom" failed: '
f'VM ID {self.comp_id} must be stopped and "state" '
'must be "started" to boot from CD-ROM.'
)
_, image_info = self._image_get_by_id(
image_id=cd_rom_image_id,
)
if image_info is None:
check_errors = True
self.message(
'Check for parameter "boot.from_cdrom" failed: '
f'Image ID {cd_rom_image_id} not found.'
)
elif image_info['type'] != 'cdrom':
check_errors = True
self.message(
'Check for parameter "boot.from_cdrom" failed: '
f'Image ID {cd_rom_image_id} is not a cd-rom type.'
)
boot_order_list = aparam_boot['order']
if boot_order_list is not None:
boot_order_duplicates = set([
boot_dev for boot_dev in boot_order_list
if boot_order_list.count(boot_dev) > 1
])
if boot_order_duplicates:
check_errors = True
self.message(
'Check for parameter "boot.order" failed: '
'List of boot devices has duplicates: '
f'{boot_order_duplicates}.'
)
12 months ago
if (
9 months ago
not comp_info['imageId']
7 months ago
and self.amodule.params['state'] in (
'poweredon', 'paused', 'started',
)
12 months ago
):
10 months ago
check_errors = True
12 months ago
self.message(
'Check for parameter "state" failed: '
7 months ago
'state for a blank Compute can not be "started" or "paused".'
12 months ago
)
10 months ago
if self.amodule.params['rollback_to'] is not None:
5 months ago
if not self.is_vm_stopped_or_will_be_stopped:
10 months ago
check_errors = True
self.message(
'Check for parameter "rollback_to" failed: '
'VM must be stopped to rollback.'
)
vm_snapshot_labels = [
9 months ago
snapshot['label'] for snapshot in comp_info['snapSets']
10 months ago
]
if self.amodule.params['rollback_to'] not in vm_snapshot_labels:
check_errors = True
self.message(
f'Check for parameter "rollback_to" failed: '
f'snapshot with label '
f'{self.amodule.params["rollback_to"]} does not exist '
9 months ago
f'for VM ID {comp_id}.'
10 months ago
)
params_to_check = {
'chipset': 'chipset',
'cpu_pin': 'cpupin',
'hp_backed': 'hpBacked',
'numa_affinity': 'numaAffinity',
7 months ago
'hot_resize': 'hotResize',
10 months ago
}
for param_name, comp_field_name in params_to_check.items():
if (
self.aparams[param_name] is not None
9 months ago
and comp_info[comp_field_name] != self.aparams[param_name]
5 months ago
and not self.is_vm_stopped_or_will_be_stopped
10 months ago
):
check_errors = True
self.message(
f'Check for parameter "{param_name}" failed: '
f'VM must be stopped to change {param_name}.'
)
if self.aparams['preferred_cpu_cores'] is not None:
5 months ago
if not self.is_vm_stopped_or_will_be_stopped:
10 months ago
check_errors = True
self.message(
'Check for parameter "preferred_cpu_cores" failed: '
'VM must be stopped to change preferred_cpu_cores.'
)
if (
self.aparam_networks_has_dpdk
9 months ago
and not comp_info['hpBacked']
10 months ago
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.'
)
9 months ago
is_vm_started_or_will_be_started = (
(
comp_info['techStatus'] == 'STARTED'
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
7 months ago
'poweredon', 'present', 'started',
9 months ago
)
)
)
or (
comp_info['techStatus'] != 'STARTED'
7 months ago
and self.amodule.params['state'] in ('poweredon', 'started')
9 months ago
)
)
if self.amodule.params['get_console_url']:
if not is_vm_started_or_will_be_started:
check_errors = True
self.message(
'Check for parameter "get_console_url" failed: '
'VM must be started to get console url.'
)
3 weeks ago
aparam_disks_dict = self.aparams['disks']
if aparam_disks_dict is not None:
aparam_disks = aparam_disks_dict.get('objects', [])
aparam_disks_ids = [disk['id'] for disk in aparam_disks]
7 months ago
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 = []
3 weeks ago
match aparam_disks_dict['mode']:
7 months ago
case 'detach' | 'delete':
disks_to_detach = aparam_disks_ids
case 'match':
comp_disk_ids = {
disk['id'] for disk in self.comp_info['disks']
}
3 weeks ago
disks_to_detach = comp_disk_ids - set(aparam_disks_ids)
7 months ago
if (
comp_boot_disk_id is not None
and comp_boot_disk_id in disks_to_detach
5 months ago
and not self.is_vm_stopped_or_will_be_stopped
7 months ago
):
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.'
)
3 weeks ago
if aparam_disks_dict['mode'] in ('delete', 'detach'):
for disk in aparam_disks:
for param, value in disk.items():
if param != 'id' and value is not None:
check_errors = True
self.message(
msg='Check for parameter "disks.objects" '
'failed: only disk id can be specified if '
'disks.mode is "delete" or "detach"'
)
break
7 months ago
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.'
)
9 months ago
5 months ago
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:
3 weeks ago
vm_networks = self.comp_info['interfaces']
if (
not vm_networks
and not self.is_vm_stopped_or_will_be_stopped
):
check_errors = True
self.message(
'Check for parameter "networks" failed: '
'VM must be stopped before attach it\'s first network.'
)
vm_networks_ids = [
network['netId'] for network in vm_networks
if network['type'] != self.VMNetType.EMPTY.value
]
aparam_networks_ids = [
network['id'] for network in aparam_networks
if network['type'] != self.VMNetType.EMPTY.value
]
new_networks = list(
set(aparam_networks_ids) - set(vm_networks_ids)
)
5 months ago
net_types = {net['type'] for net in aparam_networks}
3 weeks ago
if new_networks:
if not (
len(new_networks) == 1
and self.VMNetType.DPDK.value in net_types
) and not self.is_vm_stopped_or_will_be_stopped:
check_errors = True
self.message(
'Check for parameter "networks" failed: '
'VM must be stopped to attach non-DPDK network.'
)
5 months ago
if self.VMNetType.TRUNK.value in net_types:
if self.check_aparam_networks_trunk() is False:
check_errors = True
3 weeks ago
for network in aparam_networks:
if (
network['enabled'] is not None
and network['type'] not in [
self.VMNetType.VINS.value,
self.VMNetType.EXTNET.value,
self.VMNetType.DPDK.value,
self.VMNetType.SDN.value,
self.VMNetType.TRUNK.value,
]
):
check_errors = True
self.message(
'Check for parameter "networks.enabled" failed: '
'Can not enable or disable network '
f'ID {network['id']} and type {network['type']}.'
'Only networks of type VINS, EXTNET, DPDK, SDN, TRUNK '
'can be enabled or disabled.'
)
if self.check_aparam_cdrom() is False:
check_errors = True
if self.check_aparam_storage_policy_id() is False:
check_errors = True
if self.check_aparam_image_id() is False:
check_errors = True
9 months ago
if check_errors:
self.exit(fail=True)
def check_amodule_args_for_clone(self, clone_id: int, clone_dict: dict):
check_errors = False
aparam_clone_from = self.aparams['clone_from']
if (
clone_id
and clone_dict['cloneReference'] != self.vm_to_clone_id
):
check_errors = True
self.message(
'Check for parameter "name" failed: '
f'VM with name {self.aparams["name"]} '
f'already exists.'
)
if (
self.vm_to_clone_info['techStatus'] == 'STARTED'
and not aparam_clone_from['force']
):
check_errors = True
self.message(
'Check for parameter "clone_from.force" failed: '
'VM must be stopped or parameter "force" must be True '
'to clone it.'
)
aparam_snapshot = aparam_clone_from['snapshot']
snapshot_timestamps = [
snapshot['timestamp']
for snapshot in self.vm_to_clone_info['snapSets']
]
if aparam_snapshot is not None:
if (
aparam_snapshot['name'] is not None
and aparam_snapshot['name'] not in (
snapshot['label']
for snapshot in self.vm_to_clone_info['snapSets']
)
):
check_errors = True
self.message(
'Check for parameter "clone_from.snapshot.name" '
'failed: snapshot with name '
f'{aparam_snapshot["name"]} does not exist for VM ID '
f'{self.vm_to_clone_id}.'
)
if (
aparam_snapshot['timestamp'] is not None
and aparam_snapshot['timestamp'] not in snapshot_timestamps
):
check_errors = True
self.message(
'Check for parameter "clone_from.snapshot.timestamp" '
'failed: snapshot with timestamp '
f'{aparam_snapshot["timestamp"]} does not exist for '
f'VM ID {self.vm_to_clone_id}.'
)
if aparam_snapshot['datetime'] is not None:
timestamp_from_dt_str = self.dt_str_to_sec(
dt_str=aparam_snapshot['datetime']
)
if timestamp_from_dt_str not in snapshot_timestamps:
check_errors = True
self.message(
'Check for parameter "clone_from.snapshot.datetime" '
'failed: snapshot with datetime '
f'{aparam_snapshot["datetime"]} does not exist for '
f'VM ID {self.vm_to_clone_id}.'
)
10 months ago
if check_errors:
self.exit(fail=True)
12 months ago
9 months ago
def clone(self):
clone_from_snapshot = self.aparams['clone_from']['snapshot']
snapshot_timestamp, snapshot_name, snapshot_datetime = None, None, None
if clone_from_snapshot:
snapshot_timestamp = clone_from_snapshot['timestamp']
snapshot_name = clone_from_snapshot['name']
snapshot_datetime = clone_from_snapshot['datetime']
clone_id = self.compute_clone(
compute_id=self.vm_to_clone_id,
name=self.aparams['name'],
force=self.aparams['clone_from']['force'],
snapshot_timestamp=snapshot_timestamp,
snapshot_name=snapshot_name,
snapshot_datetime=snapshot_datetime,
3 weeks ago
sep_pool_name=self.aparams['clone_from']['sep_pool_name'],
sep_id=self.aparams['clone_from']['sep_id'],
storage_policy_id=self.aparams['clone_from']['storage_policy_id'],
9 months ago
)
return clone_id
5 months ago
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
3 weeks ago
def check_aparam_cdrom(self) -> bool | None:
check_errors = False
aparam_cdrom = self.aparams['cdrom']
if aparam_cdrom is not None:
mode = aparam_cdrom['mode']
if self.is_vm_stopped_or_will_be_stopped:
check_errors = True
self.message(
'Check for parameter "cdrom" failed: '
f'VM ID {self.comp_id} must be started to {mode} '
f'CD-ROM.'
)
image_id = aparam_cdrom['image_id']
match mode:
case 'insert':
if image_id is None:
check_errors = True
self.message(
'Check for parameter "cdrom.image_id" failed: '
f'cdrom.image_id must be specified '
f'if cdrom.mode is "insert".'
)
_, image_info = self._image_get_by_id(
image_id=image_id,
)
if image_info is None:
check_errors = True
self.message(
'Check for parameter "cdrom.image_id" failed: '
f'Image ID {image_id} not found.'
)
elif image_info['type'] != 'cdrom':
check_errors = True
self.message(
'Check for parameter "cdrom.image_id" failed: '
f'Image ID {image_id} is not a CD-ROM type.'
)
case 'eject':
if image_id is not None:
check_errors = True
self.message(
'Check for parameter "cdrom.image_id" failed: '
f'cdrom.image_id must not be specified '
f'if cdrom.mode is "eject".'
)
if not self.comp_info['cdImageId']:
check_errors = True
self.message(
'Check for parameter "cdrom.mode" failed: '
f'VM ID {self.comp_id} does not have CD-ROM '
'to eject.'
)
return not check_errors
def check_aparam_storage_policy_id(self) -> bool:
check_errors = False
aparam_storage_policy_id = self.aparams['storage_policy_id']
if aparam_storage_policy_id is not None:
for disk in self.comp_info['disks']:
if aparam_storage_policy_id != disk['storage_policy_id']:
check_errors = True
self.message(
msg='Check for parameter "storage_policy_id" failed: '
'storage_policy_id can not be changed for compute '
f'ID {self.comp_id} disk ID {disk['id']}'
)
return not check_errors
def check_aparam_boot_disk_redeploy(self) -> bool:
check_errors = False
disk_redeploy = self.aparams['boot']['disk_redeploy']
if disk_redeploy:
if self.aparams['storage_policy_id'] is None:
check_errors = True
self.message(
'Check for parameter "storage_policy_id" failed:'
'"storage_policy_id" must be specified to redeploy.'
)
vm_has_boot_disk = False
for disk in self.comp_info['disks']:
if disk['type'] == 'B':
vm_has_boot_disk = True
break
if not vm_has_boot_disk:
check_errors = True
self.message(
'Check for parameter "boot.redeploy" failed: '
'VM does not have boot disk to redeploy.'
)
aparam_disks = self.amodule.params['disks']
if aparam_disks is not None and aparam_disks['mode'] == 'match':
check_errors = True
self.message(
'Check for parameter "disks.mode" failed: '
'"disks.mode" must not be "match" to redeploy.'
)
return not check_errors
def check_aparam_image_id(self) -> bool:
check_errors = False
aparam_image_id = self.aparams['image_id']
if aparam_image_id is not None:
if aparam_image_id != self.comp_info['imageId']:
if (
self.aparams['boot'] is None
or self.aparams['boot']['disk_redeploy'] is None
):
check_errors = True
self.message(
'Check for parameter "image_id" failed: '
'"boot.disk_redeploy" must be set to True to change '
'VM image.'
)
return not check_errors
5 months ago
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
# 3) if VM does not exist, check if there is enough resources to deploy it in the target account / vdc
# 4) if VM exists: check desired state, desired configuration -> initiate action accordingly
# 5) VM does not exist: check desired state -> initiate action accordingly
# - create VM: check if target VDC exists, create VDC as necessary, create VM
# - delete VM: delete VM
# - change power state: change as required
# - change guest OS state: change as required
# 6) report result to Ansible
def main():
# Initialize DECORT KVM VM instance object
# This object does not necessarily represent an existing KVM VM
12 months ago
subj = decort_kvmvm()
amodule = subj.amodule
if subj.comp_id:
3 weeks ago
if subj.comp_info['status'] in ("MIGRATING", "DELETING", "DESTROYING", "ERROR", "REDEPLOYING"):
# cannot do anything on the existing Compute in the listed states
subj.error() # was subj.nop()
elif subj.comp_info['status'] in ("ENABLED", "DISABLED"):
if amodule.params['state'] == 'absent':
subj.destroy()
12 months ago
else:
if amodule.params['state'] in (
7 months ago
'paused', 'poweredon', 'poweredoff',
'halted', 'started', 'stopped',
12 months ago
):
subj.compute_powerstate(
comp_facts=subj.comp_info,
target_state=amodule.params['state'],
)
subj.modify(arg_wait_cycles=7)
elif subj.comp_info['status'] == "DELETED":
7 months ago
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?
12 months ago
_, subj.comp_info, _ = subj.compute_find(
comp_id=subj.comp_id,
need_custom_fields=True,
9 months ago
need_console_url=amodule.params['get_console_url'],
12 months ago
)
subj.modify()
elif amodule.params['state'] == 'absent':
# subj.nop()
# subj.comp_should_exist = False
subj.destroy()
7 months ago
elif amodule.params['state'] in (
'paused', 'poweredoff', 'halted', 'stopped'
):
subj.error()
elif subj.comp_info['status'] == "DESTROYED":
7 months ago
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()
subj.comp_should_exist = False
elif amodule.params['state'] == 'paused':
subj.error()
else:
12 months ago
state = amodule.params['state']
if state is None:
state = 'present'
# Preexisting Compute of specified identity was not found.
# If requested state is 'absent' - nothing to do
12 months ago
if state == 'absent':
subj.nop()
7 months ago
elif state in (
'present', 'poweredon', 'poweredoff',
'halted', 'started', 'stopped',
):
subj.create() # this call will also handle data disk & network connection
12 months ago
elif state == 'paused':
subj.error()
if subj.result['failed']:
amodule.fail_json(**subj.result)
else:
# prepare Compute facts to be returned as part of decon.result and then call exit_json(...)
rg_facts = None
if subj.comp_should_exist:
5 months ago
if (
(subj.result['changed'] and not subj.skip_final_get)
or subj.force_final_get
):
# There were changes to the Compute - refresh Compute facts.
12 months ago
_, subj.comp_info, _ = subj.compute_find(
comp_id=subj.comp_id,
need_custom_fields=True,
9 months ago
need_console_url=amodule.params['get_console_url'],
5 months ago
need_snapshot_merge_status=amodule.params['get_snapshot_merge_status'], # noqa: E501
12 months ago
)
#
# We no longer need to re-read RG facts, as all network info is now available inside
# compute structure
# _, rg_facts = subj.rg_find(arg_account_id=0, arg_rg_id=subj.rg_id)
subj.result['facts'] = subj.package_facts(amodule.check_mode)
amodule.exit_json(**subj.result)
if __name__ == "__main__":
main()