#!/usr/bin/python # # Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible # Copyright: (c) 2018-2021 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_disk short_description: Manage Disks (virtualized storage resources) in DECORT cloud description: > This module can be used to create new disk in DECORT cloud platform, obtain or modify its characteristics, and delete it. version_added: "2.2" author: - Sergey Shubin requirements: - python >= 2.6 - PyJWT Python module - requests Python module - netaddr Python module - decort_utils utility library (module) - DECORT cloud platform version 3.6.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, which owns this disk. 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. default: 0 required: no account_name: description: - 'Name of the account, which will own this disk.' - 'This parameter is ignored if I(account_id) is specified.' default: empty string required: no annotation: description: - Optional text description of this disk. 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 authenticator: description: - Authentication mechanism to be used when accessing DECORT controller and authorizing API call. default: jwt choices: [ jwt, oauth2, legacy ] required: yes controller_url: description: - URL of the DECORT controller that will be contacted to manage the RG according to the specification. - 'This parameter is always required regardless of the specified I(authenticator) type.' required: yes id: description: - `ID of the disk to manage. If I(id) is specified it is assumed, that this disk already exists. In other words, you cannot create new disk by specifying its ID, use I(name) when creating new disk.` - `If non-zero I(id) is specified, then I(name), I(account_id) and I(account_name) are ignored.` default: 0 required: no name: description: - `Name of the disk to manage. To manage disk by name you also need to specify either I(account_id) or I(account_name).` - If non-zero I(id) is specified, I(name) is ignored. - `Note that the platform does not enforce uniqueness of disk names, so if more than one disk with this name exists under the specified account, module will return the first occurence.` default: empty string required: no force_detach: description: - `By default it is not allowed to delete or destroy disk that is currently attached to a compute instance (e.g. virtual machine or bare metal server). Set this argument to true to change this behavior.` - This argument is meaningful for I(state=absent) operations only and ignored otherwise. default: false 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 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 place_with: description: - `This argument can be used to simplify data disks creation along with a new compute, by placing disks in the same storage, where corresponding OS image is deployed.` - `Specify ID of an OS image, and the newly created disk will be provisioned from the same storage, where this OS image is located. You may optionally specify I(pool) to control actual disk placement within that storage, or leave I(pool=default) to let platform manage it automatically.` - This parameter is used when creating new disks and ignored for all other operations. - This is an alternative to specifying I(sep_id). default: 0 required: no pool: description: - Name of the pool where to place new disk. Once disk is created, its pool cannot be changed. - This parameter is used when creating new disk and igonred for all other operations. default: empty string required: no sep_id: description: - `ID of the Storage Endpoint Provider (SEP) where to place new disk. Once disk is created, its SEP cannot be changed.` - `You may think of SEP as an identifier of a storage system connected to DECORT platform. There may be several different storage systems and, consequently, several SEPs available to choose from.` - This parameter is used when creating new disk and igonred for all other operations. - See also I(place_with) for an alternative way to specify disk placement. default: 0 required: no size: description: - Size of the disk in GB. This parameter is mandatory when creating new disk. - `If specified for an existing disk, and it is greater than current disk size, platform will try to resize the disk on the fly. Downsizing disk is not allowed.` required: no limitIO: description: - Disk input / output limit, used to limit the speed of interaction with the disk. required: no type: description: - Type of the disk. - `Disks can be of the following types: "D"-Data, "B"-Boot, "T"-Tmp.` default: "D" required: no state: description: - Specify the desired state of the disk at the exit of the module. - 'If desired I(state=present):' - ' - Disk does not exist or is in [DESTROYED, PURGED] states, create new disk according to the specifications.' - ' - Disk is in DELETED state, restore it and change size if necessary.' - ' - Disk is in one of [CREATED, ASSIGNED] states, do nothing.' - ' - Disk in any other state, abort with an error.' - 'If desired I(state=absent):' - ' - Disk is in one of [CREATED, ASSIGNED, DELETED] states, destroy it.' - ' - Disk not found or in [DESTROYED, PURGED] states, do nothing.' - ' - Disk in any other state, abort with an error.' default: present choices: [ absent, present ] 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 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 new Disk named "MyDataDisk01" of size 50 GB, on SEP ID 1, in default pool, under the account "MyAccount". decort_vins: authenticator: oauth2 app_id: "{{ MY_APP_ID }}" app_secret: "{{ MY_APP_SECRET }}" controller_url: "https://cloud.digitalenergy.online" name: "MyDataDisk01" sep_id: 1 size: 50 account_name: "MyAccount" state: present delegate_to: localhost register: my_disk ''' RETURN = ''' facts: description: facts about the disk returned: always type: dict sample: facts: id: 50 name: data01 size: 10 sep_id: 1 pool: datastore state: ASSIGNED account_id: 7 attached_to: 18 gid: 1001 ''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.decort_utils import * class decort_disk(DecortController): def __init__(self,arg_amodule): super(decort_disk, self).__init__(arg_amodule) validated_acc_id = 0 validated_acc_info = None validated_disk_id = 0 self.disk_id = 0 self.account_id = 0 validated_disk_facts = None # limitIO check for exclusive parameters if arg_amodule.params['limitIO']: self.disk_check_iotune_arg(arg_amodule.params['limitIO']) if arg_amodule.params['id'] or arg_amodule.params['name']: if arg_amodule.params['account_id'] or arg_amodule.params['account_name'] : validated_acc_id,validated_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['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.amodule.fail_json(**self.result) else: self.acc_id = validated_acc_id self.acc_info = validated_acc_info validated_disk_id,validated_disk_facts = self.disk_find( disk_id=arg_amodule.params['id'], name=arg_amodule.params['name'] if "name" in arg_amodule.params else "", account_id=self.acc_id, check_state=False, ) else: self.result['failed'] = True self.result['msg'] = ("Cannot manage Disk when its ID is 0 and name is empty") self.amodule.fail_json(**self.result) if arg_amodule.params['place_with']: image_id, image_facts = self.image_find(arg_amodule.params['place_with'], "", 0) arg_amodule.params['sep_id'] = image_facts['sepId'] self.disk_id = validated_disk_id self.disk_info = validated_disk_facts def create(self): self.disk_id = self.disk_create(accountId=self.acc_id, name = self.amodule.params['name'], description=self.amodule.params['annotation'], size=self.amodule.params['size'], type=self.amodule.params['type'], iops=self.amodule.params['iops'], sep_id=self.amodule.params['sep_id'], pool=self.amodule.params['pool'], ) #IO tune if self.amodule.params['limitIO']: self.disk_limitIO(disk_id=self.disk_id, limits=self.amodule.params['limitIO']) #set share status if self.amodule.params['shareable'] and self.amodule.params['type'] == "D": self.disk_share(self.disk_id,self.amodule.params['shareable']) return def action(self,restore=False): #restore never be done if restore: self.disk_restore(self.disk_id) #rename if id present if self.amodule.params['name'] != self.disk_info['name']: self.disk_rename(disk_id=self.disk_id, name=self.amodule.params['name']) self.disk_info['name'] = self.amodule.params['name'] #resize if self.amodule.params['size'] != self.disk_info['sizeMax']: self.disk_resize(self.disk_info,self.amodule.params['size']) #IO TUNE if self.amodule.params['limitIO']: clean_io = [param for param in self.amodule.params['limitIO'] \ if self.amodule.params['limitIO'][param] == None] for key in clean_io: del self.amodule.params['limitIO'][key] if self.amodule.params['limitIO'] != self.disk_info['iotune']: self.disk_limitIO(self.disk_id,self.amodule.params['limitIO']) #share check/update #raise Exception(self.amodule.params['shareable']) if self.amodule.params['shareable'] != self.disk_info['shareable'] and \ self.amodule.params['type'] == "D": self.disk_share(self.disk_id,self.amodule.params['shareable']) return def delete(self): self.disk_id = self.disk_delete(disk_id=self.disk_id, detach=self.amodule.params['force_detach'], permanently=self.amodule.params['permanently'], reason=self.amodule.params['reason']) self.disk_info['status'] = "DELETED" return def rename(self): self.disk_rename(diskId = self.disk_id, name = self.amodule.params['name']) self.disk_info['name'] = self.amodule.params['name'] return def nop(self): self.result['failed'] = False self.result['changed'] = False if self.disk_id: self.result['msg'] = ("No state change required for Disk ID {} because of its " "current status '{}'.").format(self.disk_id, self.disk_info['status']) else: self.result['msg'] = ("No state change to '{}' can be done for " "non-existent Disk.").format(self.amodule.params['state']) return def package_facts(self, check_mode=False): ret_dict = dict(id=0, name="none", state="CHECK_MODE", size=0, account_id=0, sep_id=0, pool="none", attached_to=0, gid=0 ) if check_mode or self.disk_info is None: return ret_dict # remove io param with zero value clean_io = [param for param in self.disk_info['iotune'] if self.disk_info['iotune'][param] == 0] for key in clean_io: del self.disk_info['iotune'][key] ret_dict['id'] = self.disk_info['id'] ret_dict['name'] = self.disk_info['name'] ret_dict['size'] = self.disk_info['sizeMax'] ret_dict['state'] = self.disk_info['status'] ret_dict['account_id'] = self.disk_info['accountId'] ret_dict['sep_id'] = self.disk_info['sepId'] ret_dict['pool'] = self.disk_info['pool'] ret_dict['attached_to'] = self.disk_info['vmid'] ret_dict['gid'] = self.disk_info['gid'] ret_dict['iotune'] = self.disk_info['iotune'] return ret_dict @staticmethod def build_parameters(): """Build and return a dictionary of parameters expected by decort_disk module in a form accepted by AnsibleModule utility class.""" return dict( account_id=dict(type='int', required=False, default=0), account_name=dict(type='str', required=False, default=''), annotation=dict(type='str', required=False, default='Disk by decort_disk'), app_id=dict(type='str', required=False, fallback=(env_fallback, ['DECORT_APP_ID'])), app_secret=dict(type='str', required=False, fallback=(env_fallback, ['DECORT_APP_SECRET']), no_log=True), authenticator=dict(type='str', required=True, choices=['legacy', 'oauth2', 'jwt']), controller_url=dict(type='str', required=True), id=dict(type='int', required=False, default=0), name=dict(type='str', required=False), force_detach=dict(type='bool', required=False, default=False), jwt=dict(type='str', required=False, fallback=(env_fallback, ['DECORT_JWT']), no_log=True), 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), place_with=dict(type='int', default=0), pool=dict(type='str', default=''), sep_id=dict(type='int', default=0), size=dict(type='int', default=0), type=dict(type='str', required=False, default="D", choices=['B', 'D', 'T']), iops=dict(type='int',required=False,default=2000), limitIO=dict(type='dict', options=dict( total_bytes_sec=dict(required=False,type='int'), read_bytes_sec=dict(required=False,type='int'), write_bytes_sec=dict(required=False,type='int'), total_iops_sec=dict(required=False,type='int'), read_iops_sec=dict(required=False,type='int'), write_iops_sec=dict(required=False,type='int'), total_bytes_sec_max=dict(required=False,type='int'), read_bytes_sec_max=dict(required=False,type='int'), write_bytes_sec_max=dict(required=False,type='int'), total_iops_sec_max=dict(required=False,type='int'), read_iops_sec_max=dict(required=False,type='int'), write_iops_sec_max=dict(required=False,type='int'), size_iops_sec=dict(required=False,type='int'),)), permanently=dict(type='bool', required=False, default=False), shareable=dict(type='bool', required=False, default=False), reason=dict(type='str', required=False,default='Managed by Ansible decort_disk'), state=dict(type='str', default='present', choices=['absent', 'present']), user=dict(type='str', required=False, fallback=(env_fallback, ['DECORT_USER'])), verify_ssl=dict(type='bool', required=False, default=True), workflow_callback=dict(type='str', required=False), workflow_context=dict(type='str', required=False), ) def main(): module_parameters = decort_disk.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'], ], ) decon = decort_disk(amodule) # #Full range of Disk status is as follows: # # "ASSIGNED","MODELED", "CREATING","CREATED","DELETED", "DESTROYED","PURGED", # if decon.disk_id: #disk exist if decon.disk_info['status'] in ["MODELED", "CREATING"]: decon.result['failed'] = True decon.result['changed'] = False decon.result['msg'] = ("No change can be done for existing Disk ID {} because of its current " "status '{}'").format(decon.disk_id, decon.disk_info['status']) # "ASSIGNED","CREATED","DELETED","PURGED", "DESTROYED" elif decon.disk_info['status'] in ["ASSIGNED","CREATED"]: if amodule.params['state'] == 'absent': decon.delete() elif amodule.params['state'] == 'present': decon.action() elif decon.disk_info['status'] in ["PURGED", "DESTROYED"]: #re-provision disk if amodule.params['state'] in ('present'): decon.create() else: decon.nop() elif decon.disk_info['status'] == "DELETED": if amodule.params['state'] in ('present'): decon.action(restore=True) elif (amodule.params['state'] == 'absent' and amodule.params['permanently']): decon.delete() else: decon.nop() else: # preexisting Disk was not found if amodule.params['state'] == 'absent': decon.nop() else: decon.create() if decon.result['failed']: amodule.fail_json(**decon.result) else: if decon.result['changed'] and amodule.params['state'] in ('present'): _, decon.disk_info = decon.disk_find(decon.disk_id) decon.result['facts'] = decon.package_facts(amodule.check_mode) amodule.exit_json(**decon.result) if __name__ == "__main__": main() #SHARE