#!/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()