Files
decort-ansible/library/decort_vins.py
2026-06-01 18:27:15 +03:00

588 lines
23 KiB
Python

#!/usr/bin/python
DOCUMENTATION = r'''
---
module: decort_vins
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_vins(DecortController):
def __init__(self):
super(decort_vins, self).__init__(AnsibleModule(**self.amodule_init_args))
arg_amodule = self.amodule
self.vins_id = 0
self.vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level
validated_rg_id = 0
rg_facts = None # will hold RG facts
validated_acc_id = 0
if arg_amodule.params['vins_id']:
# expect existing ViNS with the specified ID
# This call to vins_find will abort the module if no ViNS with such ID is present
self.vins_id, self._vins_info = self.vins_find(
arg_amodule.params['vins_id'],
check_state=False,
)
if self.vins_id == 0:
if arg_amodule.params['state'] == 'absent':
self.exit()
else:
self.message(
self.MESSAGES.obj_not_found(
obj='VINS',
id=arg_amodule.params['vins_id'],
)
)
self.exit(fail=True)
if self._vins_info is None:
self.result['failed'] = True
self.result['msg'] = "Specified ViNS ID {} not found.".format(arg_amodule.params['vins_id'])
self.amodule.fail_json(**self.result)
self.vins_level = "ID"
validated_acc_id = self._vins_info.account_id
validated_rg_id = self._vins_info.rg_id
elif arg_amodule.params['rg_id']:
# expect ViNS @ RG level in the RG with specified ID
self.vins_level = "RG"
# This call to rg_find will abort the module if no RG with such ID is present
validated_rg_id, rg_facts = self.rg_find(0, # account ID set to 0 as we search for RG by RG ID
arg_amodule.params['rg_id'], arg_rg_name="")
validated_acc_id = rg_facts.account_id
# This call to vins_find may return vins_id=0 if no ViNS found
self.vins_id, self._vins_info = self.vins_find(
vins_id=0,
vins_name=arg_amodule.params['vins_name'],
account_id=0,
rg_id=arg_amodule.params['rg_id'],
check_state=False,
)
# TODO: add checks and setup ViNS presence flags accordingly
pass
elif arg_amodule.params['account_id'] or arg_amodule.params['account_name'] != "":
# Specified account must be present and accessible by the user, otherwise abort the module
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['msg'] = ("Current user does not have access to the requested account "
"or non-existent account specified.")
self.amodule.fail_json(**self.result)
if arg_amodule.params['rg_name'] != "": # at this point we know that rg_id=0
# expect ViNS @ RG level in the RG with specified name under specified account
# RG with the specified name must be present under the account, otherwise abort the module
validated_rg_id, rg_facts = self.rg_find(validated_acc_id, 0, arg_amodule.params['rg_name'])
if (not validated_rg_id or rg_facts is None or
rg_facts.status in [
sdk_types.ResourceGroupStatus.DESTROYING,
sdk_types.ResourceGroupStatus.DESTROYED,
sdk_types.ResourceGroupStatus.DELETED,
sdk_types.ResourceGroupStatus.DISABLING,
sdk_types.ResourceGroupStatus.ENABLING,
]
):
self.result['failed'] = True
self.result['msg'] = "RG name '{}' not found or has invalid state.".format(arg_amodule.params['rg_name'])
self.amodule.fail_json(**self.result)
# This call to vins_find may return vins_id=0 if no ViNS with this name found under specified RG
# (account_id) set to 0, as we are looking for ViNS under RG
self.vins_id, self._vins_info = self.vins_find(
vins_id=0,
vins_name=arg_amodule.params['vins_name'],
account_id=0,
rg_id=validated_rg_id,
check_state=False,
)
self.vins_level = "RG"
# TODO: add checks and setup ViNS presence flags accordingly
else: # At this point we know for sure that rg_name="" and rg_id=0
# So we expect ViNS @ account level
# This call to vins_find may return vins_id=0 if no ViNS found
self.vins_id, self._vins_info = self.vins_find(
vins_id=0,
vins_name=arg_amodule.params['vins_name'],
account_id=validated_acc_id,
rg_id=0,
check_state=False,
)
self.vins_level = "ACC"
# TODO: add checks and setup ViNS presence flags accordingly
else:
# this is "invalid arguments combination" sink
# if we end up here, it means that module was invoked with vins_id=0 and rg_id=0
self.result['failed'] = True
self.result['msg'] = "Cannot find ViNS by name"
if arg_amodule.params['account_id'] == 0 and arg_amodule.params['account_name'] == '':
self.result['msg'] = "Cannot find ViNS by name when account name is empty and account ID is 0."
if arg_amodule.params['rg_name'] == "":
# rg_name without account specified
self.result['msg'] = "Cannot find ViNS by name when RG name is empty and RG ID is 0."
self.amodule.fail_json(**self.result)
return
self.rg_id = validated_rg_id
self.acc_id = validated_acc_id
if (
self._vins_info
and self._vins_info.status != sdk_types.VINSStatus.DESTROYED
):
self.check_amodule_args_for_change()
else:
self.check_amodule_args_for_create()
return
def create(self):
security_group_mode = self.amodule.params['security_group_mode']
if security_group_mode is None:
security_group_mode = False
self.message(
msg=self.MESSAGES.default_value_used(
param_name='security_group_mode',
default_value=security_group_mode
),
warning=True,
)
self.vins_id = self.vins_provision(
vins_name=self.amodule.params['vins_name'],
account_id=self.acc_id,
rg_id=self.rg_id,
ipcidr=self.amodule.params['ipcidr'],
ext_net_id=self.amodule.params['ext_net_id'],
ext_ip_addr=self.amodule.params['ext_ip_addr'],
desc=self.amodule.params['description'],
zone_id=self.amodule.params['zone_id'],
security_group_mode=security_group_mode,
)
if self.vins_id:
self._vins_info = self._vins_get_by_id(vins_id=self.vins_id)
if self.amodule.params['connect_to']:
self.vins_update_ifaces(
self.vins_info,
self.amodule.params['connect_to'],
)
if self.amodule.params['mgmtaddr']:
self.vins_update_mgmt(
self.vins_info,
self.amodule.params['mgmtaddr'],
)
return
def action(self, d_state='', restore=False):
if restore:
self.sdk_checkmode(self.api.cloudapi.vins.restore)(
vins_id=self.vins_info.id,
)
self._vins_info = self._vins_get_by_id(vins_id=self.vins_id)
self.vins_state(self.vins_info, 'enabled')
self._vins_info = self._vins_get_by_id(vins_id=self.vins_id)
if (
self.amodule.params['ext_net_id'] is not None
or self.amodule.params['ext_ip_addr'] is not None
):
self.vins_update_extnet(
self.vins_info,
self.amodule.params['ext_net_id'],
self.amodule.params['ext_ip_addr'],
)
if (
d_state == 'enabled'
and self.vins_info.status == sdk_types.VINSStatus.DISABLED
):
self.vins_state(self.vins_info, d_state)
self._vins_info = self._vins_get_by_id(vins_id=self.vins_id)
d_state = ''
if (
self.vins_info.status == sdk_types.VINSStatus.ENABLED
and self.vins_info.vnfdev.tech_status == (
sdk_types.VNFDevTechStatus.STARTED
)
):
self.vins_update_ifaces(
self.vins_info,
self.amodule.params['connect_to'],
)
if self.result['changed']:
self._vins_info = self._vins_get_by_id(vins_id=self.vins_id)
self.vins_update_mgmt(
self.vins_info,
self.amodule.params['mgmtaddr'],
)
if d_state != '':
self.vins_state(self.vins_info, d_state)
aparam_zone_id = self.aparams['zone_id']
if (
aparam_zone_id is not None
and aparam_zone_id != self.vins_info.zone_id
):
self.vins_migrate_to_zone(
net_id=self.vins_info.id,
zone_id=aparam_zone_id,
)
return
def delete(self):
self.sdk_checkmode(self.api.cloudapi.vins.delete)(
vins_id=self.vins_info.id,
permanently=self.amodule.params['permanently'],
)
return
def nop(self):
"""No operation (NOP) handler for ViNS management by decort_vins 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 ViNS state.
"""
self.result['failed'] = False
self.result['changed'] = False
if self._vins_info:
self.result['msg'] = (
f'No state change required for ViNS ID {self._vins_info.id} '
f'because of its "current status "{self._vins_info.status}".'
)
else:
self.result['msg'] = ("No state change to '{}' can be done for "
"non-existent ViNS instance.").format(self.amodule.params['state'])
return
def error(self):
self.result['failed'] = True
self.result['changed'] = False
if self._vins_info:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = (
f'Invalid target state "{self.amodule.params['state']}" '
f'requested for ViNS ID {self._vins_info.id} in the '
f'current status "{self._vins_info.status}"'
)
else:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = ("Invalid target state '{}' requested for non-existent "
"ViNS name '{}'").format(self.amodule.params['state'],
self.amodule.params['vins_name'])
return
def package_facts(self, arg_check_mode=False):
"""Package a dictionary of ViNS facts according to the decort_vins module specification.
This dictionary will be returned to the upstream Ansible engine at the completion of
the module run.
@param arg_check_mode: boolean that tells if this Ansible module is run in check mode
"""
ret_dict = dict(id=0,
name="none",
state="CHECK_MODE",
)
if arg_check_mode:
# in check mode return immediately with the default values
return ret_dict
if self._vins_info is None:
# if void facts provided - change state value to ABSENT and return
ret_dict['state'] = "ABSENT"
return ret_dict
return self._vins_info.model_dump()
@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',
default='',
),
ext_net_id=dict(
type='int',
),
ext_ip_addr=dict(
type='str',
),
ipcidr=dict(
type='str',
default='',
),
mgmtaddr=dict(
type='list',
default=[],
),
custom_config=dict(
type='bool',
default=False,
),
config_save=dict(
type='bool',
default=False,
),
connect_to=dict(
type='list',
default=[],
),
state=dict(
type='str',
choices=[
'absent',
'disabled',
'enabled',
'present',
],
),
rg_id=dict(
type='int',
default=0,
),
rg_name=dict(
type='str',
default='',
),
permanently=dict(
type='bool',
default=False,
),
vins_id=dict(
type='int',
default=0,
),
vins_name=dict(
type='str',
default='',
),
zone_id=dict(
type=int,
),
security_group_mode=dict(
type='bool',
),
),
supports_check_mode=True,
required_one_of=[
('vins_id', 'vins_name'),
],
)
def check_amodule_args_for_change(self):
check_errors = False
if self.check_aparam_zone_id() is False:
check_errors = True
if (
self.amodule.params['ext_ip_addr']
and self.amodule.params['ext_net_id'] is None
and self.vins_info.vnfs.gw is None
):
self.message(
msg=(
'Check for parameter "ext_net_id" failed: '
'the "ext_net_id" parameter must be specified '
'if the "ext_ip_addr" parameter is passed and '
'VINS is not connected to an external network.'
)
)
check_errors = True
if (
self.aparams['security_group_mode'] is not None
and self.vins_info.security_group_mode != self.aparams['security_group_mode']
):
self.message(
msg=(
'Check for parameter "security_group_mode" failed: '
'"security_group_mode" cannot be changed '
'for existing ViNS'
)
)
check_errors = True
if check_errors:
self.exit(fail=True)
def check_amodule_args_for_create(self):
check_errors = False
if self.check_aparam_zone_id() is False:
check_errors = True
if check_errors:
self.exit(fail=True)
# Workflow digest:
# 1) authenticate to DECORT controller & validate authentication by issuing API call - done when creating DECORTController
# 2) check if the ViNS with this id or name exists under specified account / resource group
# 3) if ViNS does not exist -> deploy
# 4) if ViNS exists: check desired state, desired configuration -> initiate action(s) accordingly
# 5) report result to Ansible
@DecortController.handle_sdk_exceptions
def run(self):
amodule = self.amodule
#
# Initial validation of module arguments is complete
#
# At this point non-zero vins_id means that we will be managing pre-existing ViNS
# Otherwise we are about to create a new one as follows:
# - if validated_rg_id is non-zero, create ViNS @ RG level
# - if validated_rg_id is zero, create ViNS @ account level
#
# When managing existing ViNS we need to account for both "static" and "transient"
# status. Full range of ViNS statii is as follows:
#
# "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING"
#
# if cconfig_save is true, only config save without other updates
vins_should_exist = False
if self._vins_info:
vins_should_exist = True
if self._vins_info.status in [
sdk_types.VINSStatus.MODELED,
sdk_types.VINSStatus.DISABLING,
sdk_types.VINSStatus.ENABLING,
sdk_types.VINSStatus.DELETING,
sdk_types.VINSStatus.DESTROYING,
]:
# error: nothing can be done to existing ViNS in the listed statii regardless of
# the requested state
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = (
f'No change can be done for existing '
f'ViNS ID {self.vins_id} because of its '
f'current status {self._vins_info.status}'
)
elif self._vins_info.status == sdk_types.VINSStatus.DISABLED:
if amodule.params['state'] == 'absent':
self.delete()
vins_should_exist = False
elif (
amodule.params['state'] is None
or amodule.params['state'] in ('present', 'disabled')
):
# update ViNS, leave in disabled state
self.action()
elif amodule.params['state'] == 'enabled':
# update ViNS and enable
self.action('enabled')
elif self._vins_info.status in [
sdk_types.VINSStatus.CREATED,
sdk_types.VINSStatus.ENABLED,
]:
if amodule.params['state'] == 'absent':
self.delete()
vins_should_exist = False
elif (
amodule.params['state'] is None
or amodule.params['state'] in ('present', 'enabled')
):
# update ViNS
self.action()
elif amodule.params['state'] == 'disabled':
# disable and update ViNS
self.action('disabled')
elif self._vins_info.status == sdk_types.VINSStatus.DELETED:
if amodule.params['state'] in ['present', 'enabled']:
# restore and enable
self.action(restore=True)
vins_should_exist = True
elif amodule.params['state'] == 'absent':
# destroy permanently
if self.amodule.params['permanently']:
self.delete()
vins_should_exist = False
elif amodule.params['state'] == 'disabled':
self.error()
vins_should_exist = False
elif self._vins_info.status == sdk_types.VINSStatus.DESTROYED:
state = amodule.params['state']
if state is None:
state = 'present'
self.message(
msg=(
f'State not specified, '
f'default value "{state}" will be used.'
),
warning=True,
)
if state in ('present', 'enabled'):
# need to re-provision ViNS;
self.create()
vins_should_exist = True
elif state == 'absent':
self.nop()
vins_should_exist = False
elif state == 'disabled':
self.error()
else:
state = amodule.params['state']
if state is None:
state = 'present'
self.message(
msg=(
f'State not specified, '
f'default value "{state}" will be used.'
),
warning=True,
)
# Preexisting ViNS was not found.
vins_should_exist = False # we will change it back to True if ViNS is created or restored
# If requested state is 'absent' - nothing to do
if state == 'absent':
self.nop()
elif state in ('present', 'enabled'):
self.check_amodule_argument('vins_name')
# as we already have account ID and RG ID we can create ViNS and get vins_id on success
self.create()
vins_should_exist = True
elif state == 'disabled':
self.error()
#
# conditional switch end - complete module run
#
if self.result['failed']:
amodule.fail_json(**self.result)
else:
# prepare ViNS facts to be returned as part of self.result and then call exit_json(...)
if self.result['changed']:
self._vins_info = self._vins_get_by_id(vins_id=self.vins_id)
self.result['facts'] = self.package_facts(amodule.check_mode)
amodule.exit_json(**self.result)
def main():
decort_vins().run()
if __name__ == '__main__':
main()