#!/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): # call superclass constructor first super(decort_kvmvm, self).__init__(AnsibleModule(**self.amodule_init_args)) arg_amodule = self.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 self.aparam_image = None 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, need_custom_fields=True) 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" aparam_nets = self.aparams['networks'] if aparam_nets: check_error = False net_types = {net['type'] for net in aparam_nets} # 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'] 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.aparam_image: # 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.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 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, need_custom_fields=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 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, need_custom_fields=True, ) 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', 'auto_start': 'autoStart', } 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={}, vnc_password="", ) 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'] 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 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', '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) @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', ), ), 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): 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) 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 # 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(): # Initialize DECORT KVM VM instance object # This object does not necessarily represent an existing KVM VM subj = decort_kvmvm() 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() elif subj.comp_info['status'] in ("ENABLED", "DISABLED"): if amodule.params['state'] == 'absent': subj.destroy() 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'): # 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, need_custom_fields=True, ) 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() 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 state == 'absent': subj.nop() elif state in ('present', 'poweredon', 'poweredoff', 'halted'): subj.create() # this call will also handle data disk & network connection elif 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, need_custom_fields=True, ) # # 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()