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.
851 lines
38 KiB
851 lines
38 KiB
#!/usr/bin/python
|
|
|
|
DOCUMENTATION = r'''
|
|
---
|
|
module: decort_kvmvm
|
|
|
|
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 *
|
|
|
|
|
|
class decort_kvmvm(DecortController):
|
|
def __init__(self, arg_amodule):
|
|
# call superclass constructor first
|
|
super(decort_kvmvm, self).__init__(arg_amodule)
|
|
|
|
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
|
|
self.comp_id = 0
|
|
self.comp_info = None
|
|
self.acc_id = 0
|
|
self.rg_id = 0
|
|
|
|
validated_acc_id =0
|
|
validated_rg_id = 0
|
|
validated_rg_facts = None
|
|
|
|
# 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:
|
|
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 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'])
|
|
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.
|
|
self.comp_id, self.comp_info, self.rg_id = self.compute_find(comp_id=arg_amodule.params['id'],
|
|
comp_name=arg_amodule.params['name'],
|
|
rg_id=validated_rg_id,
|
|
check_state=False)
|
|
|
|
if self.comp_id:
|
|
self.comp_should_exist = True
|
|
self.acc_id = self.comp_info['accountId']
|
|
|
|
check_error = False
|
|
params_to_check = {
|
|
'chipset': 'chipset',
|
|
'cpu_pin': 'cpupin',
|
|
'hp_backed': 'hpBacked',
|
|
'numa_affinity': 'numaAffinity',
|
|
}
|
|
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 self.aparams['state'] not in ('halted', 'poweredoff')
|
|
):
|
|
self.message(
|
|
f'Cannot change "{param_name}" for compute '
|
|
f'{self.comp_id} if parameter "state" is not '
|
|
f'halted or poweredoff.'
|
|
)
|
|
check_error = True
|
|
|
|
if check_error:
|
|
self.exit(fail=True)
|
|
|
|
else:
|
|
if self.aparams['chipset'] is None:
|
|
self.message(
|
|
'Check for parameter "chipset" failed: '
|
|
'chipset must be specified for a new compute.'
|
|
)
|
|
self.exit(fail=True)
|
|
|
|
return
|
|
|
|
def check_amodule_args(self):
|
|
"""
|
|
Additional Ansible Module arguments validation that
|
|
cannot be implemented using Ansible Argument spec.
|
|
"""
|
|
|
|
# Check parameter "networks" for DPDK type
|
|
aparam_nets = self.aparams['networks']
|
|
if aparam_nets:
|
|
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.'
|
|
)
|
|
self.exit(fail=True)
|
|
|
|
aparam_custom_fields = self.aparams['custom_fields']
|
|
if aparam_custom_fields is not None:
|
|
if (
|
|
aparam_custom_fields['disable']
|
|
and aparam_custom_fields['fields'] is not None
|
|
):
|
|
self.message(
|
|
'Check for parameter "custom_fields" failed: '
|
|
'"fields" cannot be set if "disable" is True.'
|
|
)
|
|
self.exit(fail=True)
|
|
|
|
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.
|
|
"""
|
|
# the following parameters must be present: cpu, ram, image_id or image_name
|
|
# 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
|
|
|
|
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:
|
|
# either image_name or image_id must be present
|
|
if (
|
|
self.check_amodule_argument('image_id', abort=False)
|
|
and self.amodule.params['image_id'] > 0
|
|
):
|
|
# find image by image ID and account ID
|
|
# image_find(self, image_id, image_name, account_id, rg_id=0, sepid=0, pool=""):
|
|
image_id, image_facts = self.image_find(
|
|
image_id=self.amodule.params['image_id'],
|
|
image_name="",
|
|
account_id=self.acc_id)
|
|
elif (
|
|
self.check_amodule_argument('image_name', abort=False)
|
|
and self.amodule.params['image_name'] != ""
|
|
):
|
|
# find image by image name and account ID
|
|
image_id, image_facts = self.image_find(
|
|
image_id=0,
|
|
image_name=self.amodule.params['image_name'],
|
|
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
|
|
#
|
|
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
|
|
|
|
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'}
|
|
]}
|
|
elif self.amodule.params['ci_user_data']:
|
|
cloud_init_params = self.amodule.params['ci_user_data']
|
|
else:
|
|
cloud_init_params = None
|
|
|
|
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'
|
|
|
|
# if we get through here, all parameters required to create new Compute instance should be at hand
|
|
|
|
# NOTE: KVM VM is created in HALTED state and must be explicitly started
|
|
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,
|
|
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=self.amodule.params['chipset'],
|
|
cpu_pin=cpu_pin,
|
|
hp_backed=hp_backed,
|
|
numa_affinity=numa_affinity)
|
|
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
|
|
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
|
|
if self.amodule.params['data_disks'] is not None:
|
|
self.compute_data_disks(
|
|
comp_dict=self.comp_info,
|
|
new_data_disks=self.amodule.params['data_disks'],
|
|
)
|
|
|
|
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
|
|
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
|
|
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
|
|
_, self.comp_info, _ = self.compute_find(comp_id=self.comp_id)
|
|
|
|
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)
|
|
self.comp_info['status'] = 'DESTROYED'
|
|
self.comp_should_exist = False
|
|
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?
|
|
_, self.comp_info, _ = self.compute_find(comp_id=self.comp_id)
|
|
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.
|
|
"""
|
|
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'],
|
|
)
|
|
|
|
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['data_disks'] is not None:
|
|
self.compute_data_disks(self.comp_info, self.amodule.params['data_disks'])
|
|
|
|
self.compute_resize(self.comp_info,
|
|
self.amodule.params['cpu'], self.amodule.params['ram'],
|
|
wait_for_state_change=arg_wait_cycles)
|
|
|
|
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'])
|
|
|
|
if self.compute_update_args:
|
|
self.compute_update(
|
|
compute_id=self.comp_info['id'],
|
|
**self.compute_update_args,
|
|
)
|
|
|
|
aparam_custom_fields = self.amodule.params['custom_fields']
|
|
if aparam_custom_fields is not None:
|
|
compute_custom_fields = self.comp_info['custom_fields']
|
|
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'],
|
|
)
|
|
|
|
return
|
|
|
|
@property
|
|
def compute_update_args(self) -> dict:
|
|
result_args = {}
|
|
|
|
params_to_check = {
|
|
'name': 'name',
|
|
'chipset': 'chipset',
|
|
'cpu_pin': 'cpupin',
|
|
'hp_backed': 'hpBacked',
|
|
'numa_affinity': 'numaAffinity',
|
|
'description': 'desc',
|
|
}
|
|
for param_name, comp_field_name in params_to_check.items():
|
|
aparam_value = self.amodule.params[param_name]
|
|
if (
|
|
aparam_value is not None
|
|
and aparam_value != self.comp_info[comp_field_name]
|
|
):
|
|
result_args[param_name] = aparam_value
|
|
|
|
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,
|
|
data_disks=[], # IDs of attached data disks; this list can be emty
|
|
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={},
|
|
chipset="",
|
|
interfaces=[],
|
|
cpu_pin="",
|
|
hp_backed="",
|
|
numa_affinity="",
|
|
custom_fields={},
|
|
)
|
|
|
|
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'])
|
|
|
|
ret_dict['cpu'] = self.comp_info['cpus']
|
|
ret_dict['ram'] = self.comp_info['ram']
|
|
|
|
ret_dict['image_id'] = self.comp_info['imageId']
|
|
|
|
for ddisk in self.comp_info['disks']:
|
|
if ddisk['type'] == 'B':
|
|
# if it is a boot disk - store its size
|
|
ret_dict['disk_size'] = ddisk['sizeMax']
|
|
elif ddisk['type'] == 'D':
|
|
# if it is a data disk - append its ID to the list of data disks IDs
|
|
ret_dict['data_disks'].append(ddisk['id'])
|
|
|
|
ret_dict['chipset'] = self.comp_info['chipset']
|
|
|
|
ret_dict['interfaces'] = self.comp_info['interfaces']
|
|
|
|
ret_dict['cpu_pin'] = self.comp_info['cpupin']
|
|
ret_dict['hp_backed'] = self.comp_info['hpBacked']
|
|
ret_dict['numa_affinity'] = self.comp_info['numaAffinity']
|
|
|
|
ret_dict['custom_fields'] = self.comp_info['custom_fields']
|
|
|
|
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
|
|
):
|
|
for parameter in (
|
|
'ssh_key',
|
|
'ssh_key_user',
|
|
'ci_user_data',
|
|
):
|
|
if self.aparams[parameter] is not None:
|
|
self.message(
|
|
f'Check for parameter "{parameter}" failed: '
|
|
f'"image_id" or "image_name" must be specified '
|
|
f'to set {parameter}.'
|
|
)
|
|
self.exit(fail=True)
|
|
|
|
if (
|
|
self.aparams['sep_id'] is not None
|
|
and self.aparams['boot_disk'] is None
|
|
):
|
|
self.message(
|
|
'Check for parameter "sep_id" failed: '
|
|
'"image_id" or "image_name" or "boot_disk" '
|
|
'must be specified to set sep_id.'
|
|
)
|
|
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',
|
|
],
|
|
),
|
|
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',
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
def check_amodule_args_for_change(self):
|
|
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:
|
|
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.'
|
|
)
|
|
self.exit(fail=True)
|
|
if new_boot_disk_size < boot_disk_size:
|
|
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"]}'
|
|
)
|
|
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
|
|
# 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():
|
|
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
|
|
|
|
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()
|
|
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'])
|
|
subj.modify(arg_wait_cycles=7)
|
|
elif subj.comp_info['status'] == "DELETED":
|
|
if amodule.params['state'] in ('present', 'poweredon'):
|
|
# 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?
|
|
_, subj.comp_info, _ = subj.compute_find(comp_id=subj.comp_id)
|
|
subj.modify()
|
|
elif amodule.params['state'] == 'absent':
|
|
# subj.nop()
|
|
# subj.comp_should_exist = False
|
|
subj.destroy()
|
|
elif amodule.params['state'] in ('paused', 'poweredoff', 'halted'):
|
|
subj.error()
|
|
elif subj.comp_info['status'] == "DESTROYED":
|
|
if amodule.params['state'] in ('present', 'poweredon', 'poweredoff', 'halted'):
|
|
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:
|
|
subj.check_amodule_args_for_create()
|
|
|
|
# Preexisting Compute of specified identity was not found.
|
|
# If requested state is 'absent' - nothing to do
|
|
if amodule.params['state'] == 'absent':
|
|
subj.nop()
|
|
elif amodule.params['state'] in ('present', 'poweredon', 'poweredoff', 'halted'):
|
|
subj.create() # this call will also handle data disk & network connection
|
|
elif amodule.params['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:
|
|
if subj.result['changed'] and not subj.skip_final_get:
|
|
# There were changes to the Compute - refresh Compute facts.
|
|
_, subj.comp_info, _ = subj.compute_find(comp_id=subj.comp_id)
|
|
#
|
|
# 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()
|