#!/usr/bin/python # # Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible # Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC # # Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt) # # # Author: Sergey Shubin (sergey.shubin@digitalenergy.online) # ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: decort_kvmvm short_description: Manage KVM virtual machine in DECORT cloud description: > This module can be used to create a KVM based virtual machine in Digital Energy cloud platform from a specified OS image, modify virtual machine's CPU and RAM allocation, change its power state, configure network port forwarding rules, restart guest OS and delete a virtual machine thus releasing corresponding cloud resources. version_added: "2.2" author: - Sergey Shubin requirements: - python >= 2.6 - PyJWT module - requests module - decort_utils utility library (module) - DECORT cloud platform version 3.4.1 or higher notes: - Environment variables can be used to pass selected parameters to the module, see details below. - Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used. - 'Similarly, JWT supplied in I(authenticator=jwt) mode should be received from Oauth2 provider trusted by the DECORT cloud controller on which this JWT will be used.' options: account_id: description: - 'ID of the account in which this VM will be created (for new VMs) or is located (for already existing VMs). This is the alternative to I(account_name) option.' - If both I(account_id) and I(account_name) specified, then I(account_name) is ignored. - If any one of I(vm_id) or I(rg_id) specified, I(account_id) is ignored. required: no account_name: description: - 'Name of the account in which this VM will be created (for new VMs) or is located (for already existing VMs).' - This parameter is ignored if I(account_id) is specified. - If any one of I(vm_id) or I(rg_id) specified, I(account_name) is ignored. required: no annotation: description: - Optional text description of this VM. default: empty string required: no app_id: description: - 'Application ID for authenticating to the DECORT controller when I(authenticator=oauth2).' - 'Required if I(authenticator=oauth2).' - 'If not found in the playbook or command line arguments, the value will be taken from DECORT_APP_ID environment variable.' required: no app_secret: description: - 'Application API secret used for authenticating to the DECORT controller when I(authenticator=oauth2).' - This parameter is required when I(authenticator=oauth2) and ignored in other modes. - 'If not found in the playbook or command line arguments, the value will be taken from DECORT_APP_SECRET environment variable.' required: no arch: description: - Architecture of the KVM VM. DECORT supports KVM hosts based on Intel x86 and IBM PowerPC hardware. - This parameter is used when new KVM VM is created and ignored for all other operations. - Module may fail if your DECORT installation does not have physical nodes of specified architecture. default: KVM_X86 choices: [ KVM_X86, KVM_PPC ] required: yes authenticator: description: - Authentication mechanism to be used when accessing DECORT controller and authorizing API call. default: jwt choices: [ jwt, oauth2, legacy ] required: yes boot_disk: description: - 'Boot disk size in GB. If this parameter is not specified for a new VM, the size of the boot disk will be set to the size of the OS image, which this VM is based on.' - Boot disk is always created in the same storage and pool, as the OS image, which this VM is based on. - Boot disk cannot be detached from VM. required: no controller_url: description: - URL of the DECORT controller that will be contacted to manage the VM according to the specification. - 'This parameter is always required regardless of the specified I(authenticator) type.' required: yes cpu: description: - Number of virtual CPUs to allocate for the VM. - This parameter is required for creating new VM and optional for other operations. - 'If you set this parameter for an existing VM, then the module will check if VM resize is necessary and do it accordingly. Note that resize operation on a running VM may generate errors as not all OS images support hot resize feature.' required: no data_disks: description: - Optional list of integer IDs of the pre-existing disks that will be attached to this VM. - These are additional disks (aka data disks) besides boot disk, which is created and attached automatically. required: no id: description: - ID of the KVM VM to manage. - 'Either I(id) or a combination of VM name I(name) and RG related parameters (either I(rg_id) or a pair of I(account_name) and I(rg_name) is required to manage an existing VM.' - 'This parameter is not required (and ignored) when creating new VM as VM ID is assigned by cloud platform automatically and cannot be changed afterwards. If existing VM is identified by I(id), then I(account_id), I(account_name), I(rg_name) or I(rg_id) parameters will be ignored.' required: no image_id: description: - ID of the OS image to use for VM provisioning. - 'This parameter is valid at VM creation time only and is ignored for operations on existing VMs.' - 'You need to know image ID, e.g. by extracting it with decort_osimage module and storing in a variable prior to calling decort_kvmvm.' - 'If both I(image_id) and I(image_name) are specified, I(image_name) will be ignored.' required: no image_name: description: - Name of the OS image to use for a new VM provisioning. - 'This parameter is valid at VM creation time only and is ignored for operations on existing VMs.' - 'The specified image name will be looked up in the target DECORT controller and error will be generated if no matching image is found.' - 'If both I(image_id) and I(image_name) are specified, I(image_name) will be ignored.' required: no jwt: description: - 'JWT (access token) for authenticating to the DECORT controller when I(authenticator=jwt).' - 'This parameter is required if I(authenticator=jwt) and ignored for other authentication modes.' - If not specified in the playbook, the value will be taken from DECORT_JWT environment variable. required: no name: description: - Name of the VM. - 'To manage VM by I(name) you also need to specify either I(rg_id) or a pair of I(rg_name) and I(account_name).' - 'If both I(name) and I(id) are specified, I(name) will be ignored and I(id) used to locate the VM.' required: no networks: description: - List of dictionaries that specifies network connections for this VM. - Structure of each element is as follows: - ' - (string) type - type of the network connection. Supported types are VINS and EXTNET.' - ' - (int) id - ID of the target network segment. It is ViNS ID for I(net_type=VINS) and external network segment ID for I(net_type=EXTNET)' - ' - (string) ip_addr - optional IP address to request for this connection. If not specified, the platform will assign valid IP address automatically.' - 'If you call decort_kvmvm module for an existing VM, the module will try to reconfigure existing network connections according to the new specification.' - If this parameter is not specified, the VM will have no connections to the network(s). required: no oauth2_url: description: - 'URL of the oauth2 authentication provider to use when I(authenticator=oauth2).' - 'This parameter is required when when I(authenticator=oauth2).' - If not specified in the playbook, the value will be taken from DECORT_OAUTH2_URL environment variable. password: description: - 'Password for authenticating to the DECORT controller when I(authenticator=legacy).' - 'This parameter is required if I(authenticator=legacy) and ignored in other authentication modes.' - If not specified in the playbook, the value will be taken from DECORT_PASSWORD environment variable. required: no ram: description: - Size of RAM in MB to allocate to the VM. - This parameter is required for creating new VM and optional for other operations. - 'If you set this parameter for an existing VM, then the module will check if VM resize is necessary and do it accordingly. Note that resize operation on a running VM may generate errors as not all OS images support hot resize feature.' required: no ssh_key: description: - 'SSH public key to be deployed on to the new VM for I(ssh_key_user). If I(ssh_key_user) is not specified, the key will not be deployed, and a warning is generated.' - This parameter is valid at VM creation time only and ignored for any operation on existing VMs. required: no ssh_key_user: description: - User for which I(ssh_key) should be deployed. - If I(ssh_key) is not specified, this parameter is ignored and a warning is generated. - This parameter is valid at VM creation time only and ignored for any operation on existing VMs. required: no state: description: - Specify the desired state of the virtual machine at the exit of the module. - 'Regardless of I(state), if VM exists and is in one of [MIGRATING, DESTROYING, ERROR] states, do nothing.' - 'If desired I(state=check):' - ' - Just check if VM exists in any state and return its current specifications.' - ' - If VM does not exist, fail the task.' - 'If desired I(state=present):' - ' - VM does not exist, create the VM according to the specifications and start it.' - ' - VM in one of [RUNNING, PAUSED, HALTED] states, attempt resize if necessary, change network if necessary.' - ' - VM in DELETED state, restore and start it.' - ' - VM in DESTROYED state, recreate the VM according to the specifications and start it.' - 'If desired I(state=poweredon):' - ' - VM does not exist, create it according to the specifications.' - ' - VM in RUNNING state, attempt resize if necessary, change network if necessary.' - ' - VM in one of [PAUSED, HALTED] states, attempt resize if necessary, change network if necessary, next start the VM.' - ' - VM in DELETED state, restore it.' - ' - VM in DESTROYED state, create it according to the specifications.' - 'If desired I(state=absent):' - ' - VM in one of [RUNNING, PAUSED, HALTED] states, destroy it.' - ' - VM in one of [DELETED, DESTROYED] states, do nothing.' - 'If desired I(state=paused):' - ' - VM in RUNNING state, pause the VM, resize if necessary, change network if necessary.' - ' - VM in one of [PAUSED, HALTED] states, resize if necessary, change network if necessary.' - ' - VM in one of [DELETED, DESTROYED] states, abort with an error.' - 'If desired I(state=poweredoff) or I(state=halted):' - ' - VM does not exist, create the VM according to the specifications and leave it in HALTED state.' - ' - VM in RUNNING state, stop the VM, resize if necessary, change network if necessary.' - ' - VM in one of [PAUSED, HALTED] states, resize if necessary, change network if necessary.' - ' - VM in DELETED state, abort with an error.' - ' - VM in DESTROYED state, recreate the VM according to the specifications and leave it in HALTED state.' default: present choices: [ present, absent, poweredon, poweredoff, halted, paused, check ] tags: description: - String of custom tags to be assigned to the VM (This feature is not implemented yet!). - These tags are arbitrary text that can be used for grouping or indexing the VMs by other applications. required: no user: description: - 'Name of the legacy user for authenticating to the DECORT controller when I(authenticator=legacy).' - 'This parameter is required when I(authenticator=legacy) and ignored for other authentication modes.' - If not specified in the playbook, the value will be taken from DECORT_USER environment variable. required: no rg_id: description: - ID of the Resource Group where a new VM will be deployed or an existing VM can be found. - 'This parameter may be required when managing VM by its I(name). If you specify I(rg_id), then I(account_name), I(account_id) and I(rg_name) will be ignored.' required: no rg_name: description: - Name of the RG where the VM will be deployed (for new VMs) or can be found (for existing VMs). - This parameter is required when managing VM by its I(name). - If both I(rg_id) and I(rg_name) are specified, I(rg_name) will be ignored. - If I(rg_name) is specified, then either I(account_name) or I(account_id) must also be set. required: no verify_ssl: description: - 'Controls SSL verification mode when making API calls to DECORT controller. Set it to False if you want to disable SSL certificate verification. Intended use case is when you run module in a trusted environment that uses self-signed certificates. Note that disabling SSL verification in any other scenario can lead to security issues, so please know what you are doing.' default: True required: no workflow_callback: description: - 'Callback URL that represents an application, which invokes this module (e.g. up-level orchestrator or end-user portal) and may except out-of-band updates on progress / exit status of the module run.' - API call at this URL will be used to relay such information to the application. - 'API call payload will include module-specific details about this module run and I(workflow_context).' required: no workflow_context: description: - 'Context data that will be included into the payload of the API call directed at I(workflow_callback) URL.' - 'This context data is expected to uniquely identify the task carried out by this module invocation so that up-level orchestrator could match returned information to the its internal entities.' required: no ''' EXAMPLES = ''' - name: create a VM named "SimpleVM" in the DECORT cloud along with VDC named "ANewVDC" if it does not exist yet. decort_kvmvm: annotation: "VM managed by decort_kvmvm module" authenticator: oauth2 app_id: "{{ MY_APP_ID }}" app_secret: "{{ MY_APP_SECRET }}" controller_url: "https://ds1.digitalenergy.online" name: SimpleVM cpu: 2 ram: 4096 boot_disk: size: 10 model: ovs pool: boot image_name: "Ubuntu 16.04 v1.1" data_disks: - size: 50 model: ovs pool: data port_forwards: - ext_port: 21022 int_port: 22 proto: tcp - ext_port: 80 int_port: 80 proto: tcp state: present tags: "PROJECT:Ansible STATUS:Test" account_name: "Development" rg_name: "ANewVDC" delegate_to: localhost register: simple_vm - name: resize the above VM to CPU 4 and remove port forward rule for port number 80. decort_kvmvm: authenticator: jwt jwt: "{{ MY_JWT }}" controller_url: "https://ds1.digitalenergy.online" name: SimpleVM cpu: 4 ram: 4096 port_forwards: - ext_port: 21022 int_port: 22 proto: tcp state: present account_name: "Development" rg_name: "ANewVDC" delegate_to: localhost register: simple_vm - name: stop existing VM identified by the VM ID and down size it to CPU:RAM 1:2048 along the way. decort_kvmvm: authenticator: jwt jwt: "{{ MY_JWT }}" controller_url: "https://ds1.digitalenergy.online" id: "{{ TARGET_VM_ID }}" cpu: 1 ram: 2048 state: poweredoff delegate_to: localhost register: simple_vm - name: check if VM exists and read in its specs. decort_kvmvm: authenticator: oauth2 app_id: "{{ MY_APP_ID }}" app_secret: "{{ MY_APP_SECRET }}" controller_url: "https://ds1.digitalenergy.online" name: "{{ TARGET_VM_NAME }}" rg_name: "{{ TARGET_VDC_NAME }}" account_name: "{{ TRAGET_TENANT }}" state: check delegate_to: localhost register: existing_vm ''' RETURN = ''' facts: description: facts about the virtual machine that may be useful in the playbook returned: always type: dict sample: facts: id: 9454 name: TestVM state: RUNNING username: testuser password: Yab!tWbyPF int_ip: 192.168.103.253 rg_name: SandboxVDC rg_id: 2883 vdc_ext_ip: 185.193.143.151 ext_ip: 185.193.143.106 ext_netmask: 24 ext_gateway: 185.193.143.1 ext_mac: 52:54:00:00:1a:24 ''' 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.comp_should_exist = 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: if self.comp_info['status'] != 'DESTROYED' and self.comp_info['arch'] not in ["KVM_X86", "KVM_PPC"]: # If we found a Compute in a non-DESTROYED state and it is not of type KVM_*, abort the module self.result['failed'] = True self.result['msg'] = ("Compute ID {} architecture '{}' is not supported by " "decort_kvmvm module.").format(self.comp_id, self.amodule.params['arch']) self.amodule.fail_json(**self.result) # fail the module - exit self.comp_should_exist = True self.acc_id = self.comp_info['accountId'] return 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') if self.amodule.params['arch'] not in ["KVM_X86", "KVM_PPC"]: self.result['failed'] = True self.result['msg'] = ("Unsupported architecture '{}' is specified for " "KVM VM create.").format(self.amodule.params['arch']) self.amodule.fail_json(**self.result) # fail the module - exit validated_bdisk_size = 0 image_facts = None # 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_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_facts = self.image_find(image_id=0, image_name=self.amodule.params['image_name'], account_id=self.acc_id) else: # neither image_name nor image_id are set - abort the script self.result['failed'] = True self.result['msg'] = "Missing both 'image_name' and 'image_id'. You need to specify one to create a Compute." self.amodule.fail_json(**self.result) # fail the module - exit if ((not self.check_amodule_argument('boot_disk', False)) or self.amodule.params['boot_disk'] <= 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'] else: validated_bdisk_size =self.amodule.params['boot_disk'] start_compute = True if self.amodule.params['state'] in ('halted', 'poweredoff'): start_compute = False if self.amodule.params['ssh_key'] and self.amodule.params['ssh_key_user']: cloud_init_params = {'users': [ {"name": self.amodule.params['ssh_key_user'], "ssh-authorized-keys": [self.amodule.params['ssh_key']], "shell": '/bin/bash'} ]} else: cloud_init_params = None # if we get through here, all parameters required to create new Compute instance should be at hand self.comp_id = self.kvmvm_provision(rg_id=self.rg_id, comp_name=self.amodule.params['name'], arch=self.amodule.params['arch'], cpu=self.amodule.params['cpu'], ram=self.amodule.params['ram'], boot_disk=validated_bdisk_size, image_id=image_facts['id'], annotation=self.amodule.params['annotation'], userdata=cloud_init_params, start_on_create=start_compute) self.comp_should_exist = True # Need to re-read comp_info after VM was provisioned _, self.comp_info, _ = self.compute_find(self.comp_id) # # Compute was created # # Setup network connections self.compute_networks(self.comp_info, self.amodule.params['networks']) # Next manage data disks self.compute_data_disks(self.comp_info, self.amodule.params['data_disks']) # read in Compute facts after all initial setup is complete _, self.comp_info, _ = self.compute_find(comp_id=self.comp_id) 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. """ self.compute_networks(self.comp_info, self.amodule.params['networks']) self.compute_bootdisk_size(self.comp_info, self.amodule.params['boot_disk']) 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) return 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. ) 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 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']) return ret_dict @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=''), annotation=dict(type='str', default='', 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), arch=dict(type='str', choices=['KVM_X86', 'KVM_PPC'], default='KVM_X86'), authenticator=dict(type='str', required=True, choices=['legacy', 'oauth2', 'jwt']), boot_disk=dict(type='int', 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', default=[], 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', default=[], required=False), # list of dictionaries 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), 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), ) # 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: 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'] == "ENABLED": 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 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: # 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']: # There were changes to the Compute - refresh Compute facts. _, subj.comp_info, _ = subj.compute_find(comp_id=subj.comp_id) # # TODO: check if we really need to get RG facts here in view of DNF implementation # we need to extract RG facts regardless of 'changed' flag, as it is our source of information on # the VDC external IP address _, 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()