This commit is contained in:
2025-03-03 17:44:25 +03:00
parent e537eadda6
commit f8c32d609b
27 changed files with 10248 additions and 196 deletions

View File

@@ -37,15 +37,62 @@ class decort_kvmvm(DecortController):
validated_rg_id = 0
validated_rg_facts = None
self.vm_to_clone_id = 0
self.vm_to_clone_info = None
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'],
)
)
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'],
rg_id=self.vm_to_clone_info['rgId'],
)
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()
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.
if arg_amodule.params['name'] == "" and arg_amodule.params['id'] == 0:
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
if not arg_amodule.params['id']: # manage Compute by name -> need RG identity
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'],
arg_amodule.params['account_id'])
@@ -78,18 +125,21 @@ class decort_kvmvm(DecortController):
# 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.
self.comp_id, self.comp_info, self.rg_id = self.compute_find(comp_id=arg_amodule.params['id'],
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,
check_state=False,
need_custom_fields=True)
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']
self.check_amodule_args_for_change()
else:
if self.amodule.params['state'] != 'absent':
self.check_amodule_args_for_create()
return
def check_amodule_args(self):
@@ -215,7 +265,11 @@ class decort_kvmvm(DecortController):
# each of the following calls will abort if argument is missing
self.check_amodule_argument('cpu')
self.check_amodule_argument('ram')
validated_bdisk_size = self.amodule.params['boot_disk'] or 0
validated_bdisk_size = 0
if self.amodule.params['boot'] is not None:
validated_bdisk_size = self.amodule.params['boot'].get(
'disk_size', 0
)
image_id, image_facts = None, None
if self.aparam_image:
@@ -295,7 +349,7 @@ class decort_kvmvm(DecortController):
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=validated_bdisk_size,
boot_disk_size=validated_bdisk_size,
image_id=image_id,
description=self.amodule.params['description'],
userdata=cloud_init_params,
@@ -340,10 +394,10 @@ class decort_kvmvm(DecortController):
new_networks=self.amodule.params['networks'],
)
# Next manage data disks
if self.amodule.params['data_disks'] is not None:
self.compute_data_disks(
if self.amodule.params['disks'] is not None:
self.compute_disks(
comp_dict=self.comp_info,
new_data_disks=self.amodule.params['data_disks'],
aparam_disks=self.amodule.params['disks'],
)
self.compute_affinity(self.comp_info,
@@ -372,6 +426,7 @@ class decort_kvmvm(DecortController):
_, self.comp_info, _ = self.compute_find(
comp_id=self.comp_id,
need_custom_fields=True,
need_console_url=self.amodule.params['get_console_url'],
)
if self.compute_update_args:
@@ -404,6 +459,7 @@ class decort_kvmvm(DecortController):
_, self.comp_info, _ = self.compute_find(
comp_id=self.comp_id,
need_custom_fields=True,
need_console_url=self.amodule.params['get_console_url'],
)
self.modify()
self.comp_should_exist = True
@@ -435,12 +491,27 @@ class decort_kvmvm(DecortController):
order_changing=self.aparams['network_order_changing'],
)
boot_disk_new_size = self.amodule.params['boot_disk']
if boot_disk_new_size:
self.compute_bootdisk_size(self.comp_info, boot_disk_new_size)
if self.amodule.params['disks'] is not None:
self.compute_disks(
comp_dict=self.comp_info,
aparam_disks=self.amodule.params['disks'],
)
if self.amodule.params['data_disks'] is not None:
self.compute_data_disks(self.comp_info, self.amodule.params['data_disks'])
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)
self.compute_resize(self.comp_info,
self.amodule.params['cpu'], self.amodule.params['ram'],
@@ -530,6 +601,8 @@ class decort_kvmvm(DecortController):
vnc_password="",
snapshots=[],
preferred_cpu_cores=[],
clones=[],
clone_reference=0,
)
if check_mode or self.comp_info is None:
@@ -600,6 +673,12 @@ class decort_kvmvm(DecortController):
ret_dict['preferred_cpu_cores'] = self.comp_info['preferredCpu']
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']
return ret_dict
def check_amodule_args_for_create(self):
@@ -641,11 +720,12 @@ class decort_kvmvm(DecortController):
if (
self.aparams['sep_id'] is not None
and self.aparams['boot_disk'] is None
and self.aparams['boot'] is None
and self.aparams['boot']['disk_size'] is None
):
self.message(
'Check for parameter "sep_id" failed: '
'"image_id" or "image_name" or "boot_disk" '
'"image_id" or "image_name" or "boot.disk_size" '
'must be specified to set sep_id.'
)
self.exit(fail=True)
@@ -680,8 +760,16 @@ class decort_kvmvm(DecortController):
description=dict(
type='str',
),
boot_disk=dict(
type='int',
boot=dict(
type='dict',
options=dict(
disk_id=dict(
type='int',
),
disk_size=dict(
type='int',
),
),
),
sep_id=dict(
type='int',
@@ -696,8 +784,24 @@ class decort_kvmvm(DecortController):
cpu=dict(
type='int',
),
data_disks=dict( # list of integer disk IDs
type='list',
disks=dict(
type='dict',
options=dict(
mode=dict(
type='str',
choices=[
'update',
'detach',
'delete',
'match',
],
default='update',
),
ids=dict(
type='list',
elements='int',
),
),
),
id=dict(
type='int',
@@ -836,40 +940,135 @@ class decort_kvmvm(DecortController):
type='list',
elements='int',
),
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'),
],
),
),
),
),
supports_check_mode=True,
required_one_of=[
('id', 'name'),
],
required_by={
'clone_from': 'name',
},
)
def check_amodule_args_for_change(self):
check_errors = False
new_boot_disk_size = self.amodule.params['boot_disk']
if new_boot_disk_size is not None:
for disk in self.comp_info['disks']:
if disk['type'] == 'B':
boot_disk_size = disk['sizeMax']
break
else:
check_errors = True
self.message(
f'Can\'t set boot disk size for Compute '
f'{self.comp_info["id"]}, because it doesn\'t have a '
f'boot disk.'
)
comp_info = self.vm_to_clone_info or self.comp_info
comp_id = comp_info['id']
if new_boot_disk_size < boot_disk_size:
check_errors = True
self.message(
f'New boot disk size {new_boot_disk_size} is less than '
f'current {boot_disk_size} for Compute ID '
f'{self.comp_info["id"]}'
)
aparam_boot = self.amodule.params['boot']
if aparam_boot is not None:
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}'
)
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}.'
)
if (
not self.comp_info['imageId']
not comp_info['imageId']
and self.amodule.params['state'] in ('poweredon', 'paused')
):
check_errors = True
@@ -880,7 +1079,7 @@ class decort_kvmvm(DecortController):
is_vm_stopped_or_will_be_stopped = (
(
self.comp_info['techStatus'] == 'STOPPED'
comp_info['techStatus'] == 'STOPPED'
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
@@ -889,7 +1088,7 @@ class decort_kvmvm(DecortController):
)
)
or (
self.comp_info['techStatus'] != 'STOPPED'
comp_info['techStatus'] != 'STOPPED'
and self.amodule.params['state'] in (
'halted', 'poweredoff',
)
@@ -905,7 +1104,7 @@ class decort_kvmvm(DecortController):
)
vm_snapshot_labels = [
snapshot['label'] for snapshot in self.comp_info['snapSets']
snapshot['label'] for snapshot in comp_info['snapSets']
]
if self.amodule.params['rollback_to'] not in vm_snapshot_labels:
check_errors = True
@@ -913,7 +1112,7 @@ class decort_kvmvm(DecortController):
f'Check for parameter "rollback_to" failed: '
f'snapshot with label '
f'{self.amodule.params["rollback_to"]} does not exist '
f'for VM ID{self.comp_info["id"]}.'
f'for VM ID {comp_id}.'
)
params_to_check = {
@@ -925,7 +1124,7 @@ class decort_kvmvm(DecortController):
for param_name, comp_field_name in params_to_check.items():
if (
self.aparams[param_name] is not None
and self.comp_info[comp_field_name] != self.aparams[param_name]
and comp_info[comp_field_name] != self.aparams[param_name]
and not is_vm_stopped_or_will_be_stopped
):
check_errors = True
@@ -944,7 +1143,7 @@ class decort_kvmvm(DecortController):
if (
self.aparam_networks_has_dpdk
and not self.comp_info['hpBacked']
and not comp_info['hpBacked']
and not self.aparams['hp_backed']
):
check_errors = True
@@ -954,9 +1153,149 @@ class decort_kvmvm(DecortController):
'to a DPDK network.'
)
is_vm_started_or_will_be_started = (
(
comp_info['techStatus'] == 'STARTED'
and (
self.amodule.params['state'] is None
or self.amodule.params['state'] in (
'poweredon', 'present',
)
)
)
or (
comp_info['techStatus'] != 'STARTED'
and self.amodule.params['state'] == 'poweredon'
)
)
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.'
)
aparam_disks = self.aparams['disks']
if aparam_disks is not None:
if self.comp_info['snapSets']:
match aparam_disks['mode']:
case 'detach' | 'delete':
check_errors = True
self.message(
f'Check for parameter "disks" failed: '
f'cannot {aparam_disks["mode"]} disks for '
f'Compute ID {self.comp_id} while snapshots '
f'exist in compute.'
)
case 'match':
comp_disk_ids = {
disk['id'] for disk in self.comp_info['disks']
}
disks = set(aparam_disks['ids'])
disks_to_detach = comp_disk_ids - disks
if disks_to_detach:
check_errors = True
self.message(
f'Check for parameter "disks" failed: '
f'disks {disks_to_detach} cannot be detached '
f'from Compute ID {self.comp_id} while '
f'snapshots exist in compute.'
)
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}.'
)
if check_errors:
self.exit(fail=True)
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,
)
return clone_id
# 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
@@ -976,8 +1315,6 @@ def main():
amodule = subj.amodule
if subj.comp_id:
subj.check_amodule_args_for_change()
if subj.comp_info['status'] in ("DISABLED", "MIGRATING", "DELETING", "DESTROYING", "ERROR", "REDEPLOYING"):
# cannot do anything on the existing Compute in the listed states
subj.error() # was subj.nop()
@@ -1001,6 +1338,7 @@ def main():
_, subj.comp_info, _ = subj.compute_find(
comp_id=subj.comp_id,
need_custom_fields=True,
need_console_url=amodule.params['get_console_url'],
)
subj.modify()
elif amodule.params['state'] == 'absent':
@@ -1041,6 +1379,7 @@ def main():
_, subj.comp_info, _ = subj.compute_find(
comp_id=subj.comp_id,
need_custom_fields=True,
need_console_url=amodule.params['get_console_url'],
)
#
# We no longer need to re-read RG facts, as all network info is now available inside