This commit is contained in:
2024-12-26 12:37:38 +03:00
parent 6b102946de
commit 5f3df12742
36 changed files with 11025 additions and 1259 deletions

View File

@@ -13,9 +13,10 @@ from ansible.module_utils.decort_utils import *
class decort_kvmvm(DecortController):
def __init__(self, arg_amodule):
def __init__(self):
# call superclass constructor first
super(decort_kvmvm, self).__init__(arg_amodule)
super(decort_kvmvm, self).__init__(AnsibleModule(**self.amodule_init_args))
arg_amodule = self.amodule
self.check_amodule_args()
@@ -27,6 +28,7 @@ class decort_kvmvm(DecortController):
self.comp_info = None
self.acc_id = 0
self.rg_id = 0
self.aparam_image = None
validated_acc_id =0
validated_rg_id = 0
@@ -122,17 +124,36 @@ class decort_kvmvm(DecortController):
cannot be implemented using Ansible Argument spec.
"""
# Check parameter "networks" for DPDK type
# Check parameter "networks"
aparam_nets = self.aparams['networks']
if aparam_nets:
check_error = False
net_types = {net['type'] for net in aparam_nets}
DPDK = 'DPDK'
if DPDK in net_types and not net_types.issubset({'DPDK', 'EMPTY'}):
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.'
)
# DPDK and other networks
if self.VMNetType.DPDK.value in net_types:
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.'
)
# MTU for non-DPDK networks
for net in aparam_nets:
if (
net['type'] != self.VMNetType.DPDK.value
and net['mtu'] is not None
):
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"]}).'
)
if check_error:
self.exit(fail=True)
aparam_custom_fields = self.aparams['custom_fields']
@@ -197,17 +218,7 @@ class decort_kvmvm(DecortController):
validated_bdisk_size = self.amodule.params['boot_disk'] or 0
image_id, image_facts = None, None
if (
self.amodule.params['image_id'] is None
and self.amodule.params['image_name'] is None
):
if self.amodule.params['state'] not in ('poweredoff', 'halted'):
self.result['msg'] = (
'"state" parameter for a blank Compute must be either '
'"poweredoff" or "halted".'
)
self.exit(fail=True)
else:
if self.aparam_image:
# either image_name or image_id must be present
if (
self.check_amodule_argument('image_id', abort=False)
@@ -242,7 +253,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
start_compute = False # change this once a workaround for the aforementioned libvirt "feature" is implemented
if self.amodule.params['state'] in ('halted', 'poweredoff'):
start_compute = False
@@ -331,8 +342,9 @@ class decort_kvmvm(DecortController):
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
if self.amodule.params['state'] not in ('halted', 'poweredoff'):
self.compute_powerstate(self.comp_info, 'started')
if self.aparam_image:
if self.amodule.params['state'] not in ('halted', 'poweredoff'):
self.compute_powerstate(self.comp_info, 'started')
if self.aparams['custom_fields'] is None:
custom_fields_disable = True
@@ -352,7 +364,13 @@ class decort_kvmvm(DecortController):
need_custom_fields=True,
)
self.skip_final_get = True
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
@@ -446,6 +464,7 @@ class decort_kvmvm(DecortController):
'hp_backed': 'hpBacked',
'numa_affinity': 'numaAffinity',
'description': 'desc',
'auto_start': 'autoStart',
}
for param_name, comp_field_name in params_to_check.items():
aparam_value = self.amodule.params[param_name]
@@ -457,7 +476,6 @@ class decort_kvmvm(DecortController):
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
@@ -492,6 +510,7 @@ class decort_kvmvm(DecortController):
hp_backed="",
numa_affinity="",
custom_fields={},
vnc_password="",
)
if check_mode or self.comp_info is None:
@@ -554,14 +573,36 @@ class decort_kvmvm(DecortController):
ret_dict['custom_fields'] = self.comp_info['custom_fields']
ret_dict['vnc_password'] = self.comp_info['vncPasswd']
ret_dict['auto_start'] = self.comp_info['autoStart']
return ret_dict
def check_amodule_args_for_create(self):
# Check for unacceptable parameters for a blank Compute
if (
self.aparams['image_id'] is None
and self.aparams['image_name'] is None
self.aparams['image_id'] is not None
or self.aparams['image_name'] is not None
):
self.aparam_image = True
else:
self.aparam_image = False
if (
self.aparams['state'] is not None
and self.aparams['state'] not in (
'present',
'poweredoff',
'halted',
)
):
self.message(
'Check for parameter "state" failed: '
'state for a blank Compute must be either '
'"present", "poweredoff" or "halted".'
)
self.exit(fail=True)
for parameter in (
'ssh_key',
'ssh_key_user',
@@ -586,130 +627,175 @@ class decort_kvmvm(DecortController):
)
self.exit(fail=True)
@staticmethod
def build_parameters():
"""Build and return a dictionary of parameters expected by decort_kvmvm module in a form
accepted by AnsibleModule utility class.
This dictionary is then used y AnsibleModule class instance to parse and validate parameters
passed to the module from the playbook.
"""
return dict(
account_id=dict(type='int', required=False, default=0),
account_name=dict(type='str', required=False, default=''),
description=dict(type='str', required=False),
app_id=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_APP_ID'])),
app_secret=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_APP_SECRET']),
no_log=True),
authenticator=dict(type='str',
required=True,
choices=['legacy', 'oauth2', 'jwt']),
boot_disk=dict(type='int', required=False),
sep_id=dict(type='int', required=False),
pool=dict(type='str', required=False),
controller_url=dict(type='str', required=True),
# count=dict(type='int', required=False, default=1),
cpu=dict(type='int', required=False),
# datacenter=dict(type='str', required=False, default=''),
data_disks=dict(type='list', required=False), # list of integer disk IDs
id=dict(type='int', required=False, default=0),
image_id=dict(type='int', required=False),
image_name=dict(type='str', required=False),
jwt=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_JWT']),
no_log=True),
name=dict(type='str'),
networks=dict(
type='list',
elements='dict',
options=dict(
type=dict(
type='str',
required=True,
choices=[
'VINS',
'EXTNET',
'VFNIC',
'DPDK',
'EMPTY',
],
@property
def amodule_init_args(self) -> dict:
return self.pack_amodule_init_args(
argument_spec=dict(
account_id=dict(
type='int',
default=0,
),
account_name=dict(
type='str',
default='',
),
description=dict(
type='str',
),
boot_disk=dict(
type='int',
),
sep_id=dict(
type='int',
),
pool=dict(
type='str',
),
controller_url=dict(
type='str',
required=True,
),
cpu=dict(
type='int',
),
data_disks=dict( # list of integer disk IDs
type='list',
),
id=dict(
type='int',
default=0,
),
image_id=dict(
type='int',
),
image_name=dict(
type='str',
),
name=dict(
type='str',
),
networks=dict(
type='list',
elements='dict',
options=dict(
type=dict(
type='str',
required=True,
choices=[
'VINS',
'EXTNET',
'VFNIC',
'DPDK',
'EMPTY',
],
),
id=dict(
type='int',
),
ip_addr=dict(
type='str',
),
mtu=dict(
type='int',
),
),
id=dict(
type='int',
),
ip_addr=dict(
type='str',
),
),
required_if=[
('type', 'VINS', ('id',)),
('type', 'EXTNET', ('id',)),
('type', 'VFNIC', ('id',)),
('type', 'DPDK', ('id',)),
],
),
network_order_changing=dict(
type='bool',
default=False,
),
oauth2_url=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_OAUTH2_URL'])),
password=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_PASSWORD']),
no_log=True),
ram=dict(type='int', required=False),
rg_id=dict(type='int', default=0),
rg_name=dict(type='str', default=""),
ssh_key=dict(type='str', required=False),
ssh_key_user=dict(type='str', required=False),
tag=dict(type='dict', required=False),
affinity_label=dict(type='str', required=False),
aff_rule=dict(type='list', required=False),
aaff_rule=dict(type='list', required=False),
ci_user_data=dict(type='dict', required=False),
state=dict(type='str',
default='present',
choices=['absent', 'paused', 'poweredoff', 'halted', 'poweredon', 'present', 'check']),
tags=dict(type='str', required=False),
user=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_USER'])),
verify_ssl=dict(type='bool', required=False, default=True),
# wait_for_ip_address=dict(type='bool', required=False, default=False),
workflow_callback=dict(type='str', required=False),
workflow_context=dict(type='str', required=False),
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',
required_if=[
('type', 'VINS', ('id',)),
('type', 'EXTNET', ('id',)),
('type', 'VFNIC', ('id',)),
('type', 'DPDK', ('id',)),
],
),
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',
'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',
),
),
),
auto_start=dict(
type='bool',
)
),
supports_check_mode=True,
required_one_of=[
('id', 'name'),
],
)
def check_amodule_args_for_change(self):
@@ -734,6 +820,17 @@ class decort_kvmvm(DecortController):
)
self.exit(fail=True)
if (
not self.comp_info['imageId']
and self.amodule.params['state'] in ('poweredon', 'paused')
):
self.message(
'Check for parameter "state" failed: '
'state for a blank Compute can not be "poweredon" or "paused".'
)
self.exit(fail=True)
# 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
@@ -747,49 +844,10 @@ class decort_kvmvm(DecortController):
# 6) report result to Ansible
def main():
module_parameters = decort_kvmvm.build_parameters()
amodule = AnsibleModule(argument_spec=module_parameters,
supports_check_mode=True,
mutually_exclusive=[
['oauth2', 'password'],
['password', 'jwt'],
['jwt', 'oauth2'],
],
required_together=[
['app_id', 'app_secret'],
['user', 'password'],
],
required_one_of=[
['id', 'name'],
],
)
# Initialize DECORT KVM VM instance object
# This object does not necessarily represent an existing KVM VM
subj = decort_kvmvm(amodule)
# handle state=check before any other logic
if amodule.params['state'] == 'check':
subj.result['changed'] = False
if subj.comp_id:
# Compute is found - package facts and report success to Ansible
subj.result['failed'] = False
# _, subj.comp_info, _ = subj.compute_find(comp_id=subj.comp_id)
# _, 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)
# we exit the module at this point
else:
subj.result['failed'] = True
subj.result['msg'] = ("Cannot locate Compute name '{}'. Other arguments are: Compute ID {}, "
"RG name '{}', RG ID {}, Account '{}'.").format(amodule.params['name'],
amodule.params['id'],
amodule.params['rg_name'],
amodule.params['rg_id'],
amodule.params['account_name'])
amodule.fail_json(**subj.result)
pass
subj = decort_kvmvm()
amodule = subj.amodule
if subj.comp_id:
subj.check_amodule_args_for_change()
@@ -800,8 +858,14 @@ def main():
elif subj.comp_info['status'] in ("ENABLED", "DISABLED"):
if amodule.params['state'] == 'absent':
subj.destroy()
elif amodule.params['state'] in ('present', 'paused', 'poweredon', 'poweredoff', 'halted'):
subj.compute_powerstate(subj.comp_info, amodule.params['state'])
else:
if amodule.params['state'] in (
'paused', 'poweredon', 'poweredoff', 'halted'
):
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":
if amodule.params['state'] in ('present', 'poweredon'):
@@ -830,13 +894,16 @@ def main():
else:
subj.check_amodule_args_for_create()
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
if amodule.params['state'] == 'absent':
if state == 'absent':
subj.nop()
elif amodule.params['state'] in ('present', 'poweredon', 'poweredoff', 'halted'):
elif state in ('present', 'poweredon', 'poweredoff', 'halted'):
subj.create() # this call will also handle data disk & network connection
elif amodule.params['state'] == 'paused':
elif state == 'paused':
subj.error()
if subj.result['failed']: