From 8f7c933fb8990ffb3e72c3dbbf63fb3ddbc9ed63 Mon Sep 17 00:00:00 2001 From: msbolshakov Date: Fri, 16 Sep 2022 23:26:11 +0700 Subject: [PATCH] decort_disk remake --- library/decort_disk.py | 588 +++++++++++++++++++---------------- module_utils/decort_utils.py | 272 +++++++++------- 2 files changed, 486 insertions(+), 374 deletions(-) diff --git a/library/decort_disk.py b/library/decort_disk.py index 1302969..99f993c 100644 --- a/library/decort_disk.py +++ b/library/decort_disk.py @@ -157,6 +157,16 @@ options: - `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. @@ -234,103 +244,239 @@ facts: 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,amodule): + super(decort_disk, self).__init__(amodule) -def decort_disk_package_facts(disk_facts, check_mode=False): - """Package a dictionary of disk facts according to the decort_disk module specification. - This dictionary will be returned to the upstream Ansible engine at the completion of - the module run. - - @param (dict) disk_facts: dictionary with Disk facts as returned by API call to .../disks/get - @param (bool) check_mode: boolean that tells if this Ansible module is run in check mode - """ - - 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: - # in check mode return immediately with the default values - return ret_dict + self.validated_account_id = 0 + self.validated_disk_id = 0 + self.disk_facts = None # will hold Disk facts + self.acc_facts = None # will hold Account facts + + # limitIO check for exclusive parameters + if amodule.params['limitIO']: + limit = amodule.params['limitIO'] + if limit['total_bytes_sec'] > 0 and limit['read_bytes_sec'] > 0 or limit['write_bytes_sec'] > 0: + self.result['failed'] = True + self.result['msg'] = ("total and read/write of bytes_sec cannot be set at the same time.") + amodule.fail_json(**self.result) + elif limit['total_iops_sec'] > 0 and limit['read_iops_sec'] > 0 or limit['write_iops_sec'] > 0: + self.result['failed'] = True + self.result['msg'] = ("total and read/write of iops_sec cannot be set at the same time.") + amodule.fail_json(**self.result) + elif limit['total_bytes_sec_max'] > 0 and limit['read_bytes_sec_max'] > 0 or limit['write_bytes_sec_max'] > 0: + self.result['failed'] = True + self.result['msg'] = ("total and read/write of bytes_sec_max cannot be set at the same time.") + amodule.fail_json(**self.result) + elif limit['total_iops_sec_max'] > 0 and limit['read_iops_sec_max'] > 0 or limit['write_iops_sec_max'] > 0: + self.result['failed'] = True + self.result['msg'] = ("total and read/write of iops_sec_max cannot be set at the same time.") + amodule.fail_json(**self.result) + + + if amodule.params['account_id']: + self.validated_account_id = amodule.params['account_id'] + elif amodule.params['account_name']: + self.validated_account_id, _ = self.account_find(amodule.params['account_name']) + elif not amodule.params['id'] and not amodule.params['account_name']: + self.result['failed'] = True + self.result['msg'] = ("Cannot found disk without account id or name.") + amodule.fail_json(**self.result) + + if self.validated_account_id == 0 and not amodule.params['id']: + # we failed either to find or access the specified account - fail the module + self.result['failed'] = True + self.result['msg'] = ("Cannot find account '{}'").format(amodule.params['account_name']) + amodule.fail_json(**self.result) + + if amodule.params['id'] or amodule.params['name']: + self.validated_disk_id, self.disk_facts = self.decort_disk_find(amodule) + + else: + self.result['failed'] = True + self.result['msg'] = ("Cannot find or create disk without disk name or disk id") + amodule.fail_json(**self.result) + + + def decort_disk_create(self, amodule): + if self.disk_facts['status'] in ["DESTROYED", "PURGED"]: + if not amodule.params['limitIO']: + amodule.params['limitIO'] = self.disk_facts['iotune'] + self.disk_id = self.disk_create(accountId=self.validated_account_id, gid=self.disk_facts['gid'], + name=self.disk_facts['name'], description=self.disk_facts['desc'], + size=self.disk_facts['sizeMax'], type=self.disk_facts['type'], + iops=self.disk_facts['iotune']['total_iops_sec'], + sep_id=self.disk_facts['sepId'], pool=self.disk_facts['pool']) + self.disk_facts['iotune'] = 0 + else: + self.disk_id = self.disk_create(accountId=self.validated_account_id, gid=amodule.params['gid'], + name=amodule.params['name'], description=amodule.params['description'], + size=amodule.params['size'], type=amodule.params['type'], + iops=amodule.params['iops'], + sep_id=amodule.params['sep_id'], pool=amodule.params['pool']) + self.result['failed'] = False + self.result['changed'] = True + self.result['msg'] = ("Disk with id '{}' successfully created.").format(self.disk_id) + return self.disk_id + + def decort_disk_delete(self, amodule): + self.disk_id = self.disk_delete(disk_id=self.validated_disk_id, + detach=amodule.params['force_detach'], + permanently=amodule.params['permanently'], + reason=amodule.params['reason']) + return + + + def decort_disk_find(self, amodule): + if amodule.params['id']: + self.disk_id, self.disk_facts = self.disk_find(disk_id=amodule.params['id'], + name=amodule.params['name'], + account_id=0) + elif amodule.params['name']: + self.disk_id, self.disk_facts = self.disk_find(disk_id=self.validated_disk_id, + name=amodule.params['name'], + account_id=self.validated_account_id) + + if not self.disk_id and not amodule.params['name']: + self.result['failed'] = True + self.result['msg'] = "Specified Disk ID {} not found.".format(amodule.params['id']) + amodule.fail_json(**self.result) + self.result['facts'] = decort_disk.decort_disk_package_facts(self.disk_facts) + return self.disk_id, self.disk_facts + + def decort_disk_limitIO(self, amodule): + self.limits = amodule.params['limitIO'] + + self.disk_limitIO(limits = self.limits, + diskId = self.validated_disk_id) + self.disk_facts['iotune'] = amodule.params['limitIO'] + self.result['facts'] = decort_disk.decort_disk_package_facts(self.disk_facts) + return + + def decort_disk_rename(self, amodule): + self.disk_rename(diskId = self.validated_disk_id, + name = amodule.params['name']) + self.disk_facts['name'] = amodule.params['name'] + self.result['facts'] = decort_disk.decort_disk_package_facts(self.disk_facts) + self.result['msg'] = ("Disk with id '{}',successfully renamed to '{}'.").format(self.validated_disk_id, amodule.params['name']) + return + + def decort_disk_package_facts(disk_facts, 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: + # in check mode return immediately with the default values + return ret_dict + + if disk_facts is None: + # if void facts provided - change state value to ABSENT and return + ret_dict['state'] = "ABSENT" + return ret_dict + + ret_dict['id'] = disk_facts['id'] + ret_dict['name'] = disk_facts['name'] + ret_dict['size'] = disk_facts['sizeMax'] + ret_dict['state'] = disk_facts['status'] + ret_dict['account_id'] = disk_facts['accountId'] + ret_dict['sep_id'] = disk_facts['sepId'] + ret_dict['pool'] = disk_facts['pool'] + ret_dict['attached_to'] = disk_facts['vmid'] + ret_dict['gid'] = disk_facts['gid'] + ret_dict['iotune'] = disk_facts['iotune'] - if disk_facts is None: - # if void facts provided - change state value to ABSENT and return - ret_dict['state'] = "ABSENT" return ret_dict - ret_dict['id'] = disk_facts['id'] - ret_dict['name'] = disk_facts['name'] - ret_dict['size'] = disk_facts['sizeMax'] - ret_dict['state'] = disk_facts['status'] - ret_dict['account_id'] = disk_facts['accountId'] - ret_dict['sep_id'] = disk_facts['sepId'] - ret_dict['pool'] = disk_facts['pool'] - ret_dict['attached_to'] = disk_facts['vmid'] - ret_dict['gid'] = disk_facts['gid'] - - return ret_dict - -def decort_disk_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', + def decort_disk_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_OAUTH2_URL'])), - password=dict(type='str', + 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), + gid=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', default=2000), + limitIO=dict(type='dict', + options=dict( + total_bytes_sec=dict(default=0,type='int'), + read_bytes_sec=dict(default=0,type='int'), + write_bytes_sec=dict(default=0,type='int'), + total_iops_sec=dict(default=0,type='int'), + read_iops_sec=dict(default=0,type='int'), + write_iops_sec=dict(default=0,type='int'), + total_bytes_sec_max=dict(default=0,type='int'), + read_bytes_sec_max=dict(default=0,type='int'), + write_bytes_sec_max=dict(default=0,type='int'), + total_iops_sec_max=dict(default=0,type='int'), + read_iops_sec_max=dict(default=0,type='int'), + write_iops_sec_max=dict(default=0,type='int'), + size_iops_sec=dict(default=0,type='int'),)), + permanently=dict(type='bool', required=False, default=False), + reason=dict(type='int', required=False), + description=dict(type='str', required=False, + default="Disk created with Ansible Decort_disk module."), + state=dict(type='str', + default='present', + choices=['absent', 'present']), + user=dict(type='str', required=False, - fallback=(env_fallback, ['DECORT_PASSWORD']), - no_log=True), - place_with=dict(type='int', required=False, default=0), - pool=dict(type='str', required=False, default=''), - sep_id=dict(type='int', required=False, default=0), - size=dict(type='int', required=False), - 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), - ) + 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_parameters() + module_parameters = decort_disk.decort_disk_parameters() amodule = AnsibleModule(argument_spec=module_parameters, supports_check_mode=True, @@ -345,197 +491,103 @@ def main(): ], ) - decon = DecortController(amodule) - - disk_id = 0 - disk_facts = None # will hold Disk facts - validated_acc_id = 0 - acc_facts = None # will hold Account facts - - if amodule.params['id']: - # expect existing Disk with the specified ID - # This call to disk_find will abort the module if no Disk with such ID is present - disk_id, disk_facts = decon.disk_find(amodule.params['id']) - if not disk_id: - decon.result['failed'] = True - decon.result['msg'] = "Specified Disk ID {} not found.".format(amodule.params['id']) - amodule.fail_json(**decon.result) - validated_acc_id =disk_facts['accountId'] - elif amodule.params['account_id'] > 0 or amodule.params['account_name'] != "": - # Make sure disk name is specified, if not - fail the module - if amodule.params['name'] == "": - decon.result['failed'] = True - decon.result['msg'] = ("Cannot manage disk if both ID is 0 and disk name is empty.") - amodule.fail_json(**decon.result) - # Specified account must be present and accessible by the user, otherwise abort the module - validated_acc_id, acc_facts = decon.account_find(amodule.params['account_name'], amodule.params['account_id']) - if not validated_acc_id: - decon.result['failed'] = True - decon.result['msg'] = ("Current user does not have access to the requested account " - "or non-existent account specified.") - amodule.fail_json(**decon.result) - # This call to disk_find may return disk_id=0 if no Disk with this name found in - disk_id, disk_facts = decon.disk_find(disk_id=0, disk_name=amodule.params['name'], - account_id=validated_acc_id, - check_state=False) - else: - # this is "invalid arguments combination" sink - # if we end up here, it means that module was invoked with disk_id=0 and undefined account - decon.result['failed'] = True - if amodule.params['account_id'] == 0 and amodule.params['account_name'] == "": - decon.result['msg'] = "Cannot find Disk by name when account name is empty and account ID is 0." - if amodule.params['name'] == "": - decon.result['msg'] = "Cannot find Disk by empty name." - amodule.fail_json(**decon.result) + decon = decort_disk(amodule) - # - # Initial validation of module arguments is complete - # - # At this point non-zero disk_id means that we will be managing pre-existing Disk - # Otherwise we are about to create a new disk - # - # Valid Disk model statii are as follows: - # - # "CREATED", "ASSIGNED", DELETED", "DESTROYED", "PURGED" - # - - disk_should_exist = False - target_sep_id = 0 - # target_pool = "" + if decon.validated_disk_id == 0 and amodule.params['state'] == 'present': + # if id cannot cannot be found and have a state 'present', then create a new disk + decon.validated_disk_id = decon.decort_disk_create(amodule) + _, decon.disk_facts = decon.decort_disk_find(amodule) + decon.result['changed'] = True + decon.result['msg'] = ("Disk with id '{}' successfully created.").format(decon.validated_disk_id) - if disk_id: - disk_should_exist = True - if disk_facts['status'] in ["MODELED", "CREATING" ]: - # error: nothing can be done to existing Disk in the listed statii regardless of - # the requested state - 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(disk_id, disk_facts['status']) - elif disk_facts['status'] in ["CREATED", "ASSIGNED"]: - if amodule.params['state'] == 'absent': - decon.disk_delete(disk_id, True, amodule.params['force_detach']) # delete permanently - disk_facts['status'] = 'DESTROYED' - disk_should_exist = False - elif amodule.params['state'] == 'present': - # resize Disk as necessary & if possible - if decon.check_amodule_argument('size', False): - decon.disk_resize(disk_facts, amodule.params['size']) - elif disk_facts['status'] == "DELETED": - if amodule.params['state'] == 'present': - # restore - decon.disk_restore(disk_id) - _, disk_facts = decon.disk_find(disk_id) - decon.disk_resize(disk_facts, amodule.params['size']) - disk_should_exist = True - elif amodule.params['state'] == 'absent': - # destroy permanently - decon.disk_delete(disk_id, permanently=True) - disk_facts['status'] = 'DESTROYED' - disk_should_exist = False - elif disk_facts['status'] in ["DESTROYED", "PURGED"]: - if amodule.params['state'] == 'present': - # Need to re-provision this Disk. - # Some attributes may change, some must stay the same: - # - disk name - stays, take from disk_facts - # - account ID - stays, take from validated account ID - # - size - may change, take from module arguments - # - SEP ID - may change, build based on module arguments - # - pool - may change, take from module arguments - # - annotation - may change, take from module arguments - # - # First validate required parameters: - decon.check_amodule_argument('size') # this will fail the module if size is not specified - target_sep_id = 0 - if decon.check_amodule_argument('sep_id', False) and amodule.params['sep_id'] > 0: - # non-zero sep_id is explicitly passed in module arguments - target_sep_id = amodule.params['sep_id'] - elif decon.check_amodule_argument('place_with', False) and amodule.params['place_with'] > 0: - # request to place this disk on the same SEP as the specified OS image - # validate specified OS image and assign SEP ID accordingly - image_id, image_facts = decon.image_find(amodule.params['place_with'], "", 0) - target_sep_id = image_facts['sepId'] - else: - # no new SEP ID is explicitly specified, and no place_with option - use sepId from the disk_facts - target_sep_id = disk_facts['sepId'] - disk_id = decon.disk_provision(disk_name=disk_facts['name'], # as this disk was found, its name is in the facts - size=amodule.params['size'], - account_id=validated_acc_id, - sep_id=target_sep_id, - pool=amodule.params['pool'], - desc=amodule.params['annotation'], - location="") - disk_should_exist = True - elif amodule.params['state'] == 'absent': - # nop - decon.result['failed'] = False - decon.result['changed'] = False - decon.result['msg'] = ("No state change required for Disk ID {} because of its " - "current status '{}'").format(disk_id, - disk_facts['status']) - disk_should_exist = False - else: - # disk_id =0 -> pre-existing Disk was not found. - disk_should_exist = False # we will change it back to True if Disk is created successfully - # If requested state is 'absent' - nothing to do + elif decon.validated_disk_id == 0 and amodule.params['state'] == 'absent' and amodule.params['name']: + # if disk with specified name cannot be found and have a state 'absent', then nothing to do, + # specified disk already deleted + decon.result['msg'] = ("Disk with name '{}' has already been deleted or your account does not have" + "access to it.")\ + .format(amodule.params['name']) + amodule.exit_json(**decon.result) + + elif decon.validated_disk_id == 0 and amodule.params['state'] == 'absent' and amodule.params['id']: + # if disk with specified id cannot be found and have a state 'absent', then nothing to do, + # specified disk already deleted + decon.result['msg'] = ("Disk with name '{}' has already been deleted or your account does not have" + "access to it.")\ + .format(decon.validated_disk_id) + amodule.exit_json(**decon.result) + + elif decon.disk_facts['status'] == "CREATED": + if amodule.params['state'] == 'present': + # if disk status in condition "CREATED" and state "present", nothing to do, + # specified disk already created + decon.result['msg'] = "Specified Disk ID {} already created.".format(decon.validated_disk_id) + if amodule.params['state'] == 'absent': - decon.result['failed'] = False - decon.result['changed'] = False - decon.result['msg'] = ("Nothing to do as target state 'absent' was requested for " - "non-existent Disk name '{}'").format(amodule.params['name']) - elif amodule.params['state'] == 'present': - decon.check_amodule_argument('name') # if disk name not specified, fail the module - decon.check_amodule_argument('size') # if disk size not specified, fail the module - - # as we already have account ID, we can create Disk and get disk id on success - if decon.check_amodule_argument('sep_id', False) and amodule.params['sep_id'] > 0: - # non-zero sep_id is explicitly passed in module arguments - target_sep_id = amodule.params['sep_id'] - elif decon.check_amodule_argument('place_with', False) and amodule.params['place_with'] > 0: - # request to place this disk on the same SEP as the specified OS image - # validate specified OS image and assign SEP ID accordingly - image_id, image_facts = decon.image_find(amodule.params['place_with'], "", 0) - target_sep_id = image_facts['sepId'] - else: - # no SEP ID is explicitly specified, and no place_with option - we do not know where - # to place the new disk - fail the module + # if disk status in condition "CREATED" and state "absent", delete the disk + decon.validated_disk_id = decon.decort_disk_delete(amodule) + decon.disk_facts['status'] = "DESTROYED" + decon.result['msg'] = ("Disk with id '{}' successfully deleted.").format(decon.disk_facts['id']) + decon.result['facts'] = decon.decort_disk_package_facts(decon.disk_facts) + amodule.exit_json(**decon.result) + + elif decon.disk_facts['status'] in ["MODELED", "CREATING" ]: + # if disk in status "MODELED" or "CREATING", + # then we cannot do anything, while disk in this status + decon.result['changed'] = False + decon.result['msg'] = ("Cannot do anything with disk id '{}',please wait until disk will be created.")\ + .format(decon.validated_disk_id) + amodule.fail_json(**decon.result) + + elif decon.disk_facts['status'] == "DELETED": + if amodule.params['state'] == 'present': + # if disk in "DELETED" status and "present" state, restore + decon.disk_restore(decon.validated_disk_id) + _, decon.disk_facts = decon.decort_disk_find(amodule) + decon.result['changed'] = True + decon.result['msg'] = ("Disk with id '{}',restored successfully.").format(decon.validated_disk_id) + + elif amodule.params['state'] == 'absent': + # if disk in "DELETED" status and "absent" state, nothing to do + decon.result['msg'] = "Specified Disk ID {} already destroyed.".format(decon.validated_disk_id) + amodule.exit_json(**decon.result) + + elif decon.disk_facts['status'] in ["DESTROYED", "PURGED"]: + if amodule.params['state'] == 'present': + decon.validated_disk_id = decon.decort_disk_create(amodule) + _, decon.disk_facts = decon.decort_disk_find(amodule) + decon.result['changed'] = True + decon.result['facts'] = decon.decort_disk_package_facts(decon.disk_facts) + decon.result['msg'] = ("Disk with id '{}',created successfully.").format(decon.validated_disk_id) + + elif amodule.params['state'] == 'absent': + decon.result['msg'] = "Specified Disk ID {} already destroyed.".format(decon.validated_disk_id) + amodule.exit_json(**decon.result) + + if amodule.params['state'] == "present": + if decon.disk_facts['sizeMax'] != amodule.params['size']: + if decon.disk_facts['sizeMax'] > amodule.params['size'] and amodule.params['size'] != 0: decon.result['failed'] = True - decon.result['msg'] = ("Cannot create new Disk name '{}': no SEP ID specified and " - "no 'place_with' option used.").format(amodule.params['name']) + decon.result['msg'] = ("Disk id '{}', cannot reduce disk size.").format(decon.validated_disk_id) amodule.fail_json(**decon.result) - - disk_id = decon.disk_provision(disk_name=amodule.params['name'], - size=amodule.params['size'], - account_id=validated_acc_id, - sep_id=target_sep_id, - pool_name=amodule.params['pool'], - desc=amodule.params['annotation'], - location="") - disk_should_exist = True - elif amodule.params['state'] == 'disabled': - decon.result['failed'] = True - decon.result['changed'] = False - decon.result['msg'] = ("Invalid target state '{}' requested for non-existent " - "Disk name '{}'").format(amodule.params['state'], - amodule.params['name']) + elif decon.disk_facts['sizeMax'] < amodule.params['size']: + decon.disk_resize(disk_facts=decon.disk_facts, + new_size=amodule.params['size']) + decon.result['changed'] = True + decon.result['msg'] = ("Disk with id '{}',resized successfully.").format(decon.validated_disk_id) + + if amodule.params['limitIO'] and amodule.params['limitIO'] != decon.disk_facts['iotune']: + decon.decort_disk_limitIO(amodule) + decon.result['changed'] = True + decon.result['msg'] = ("Disk with id '{}',limited successfully.").format(decon.validated_disk_id) - # - # conditional switch end - complete module run - # - if decon.result['failed']: - amodule.fail_json(**decon.result) - else: - # prepare Disk facts to be returned as part of decon.result and then call exit_json(...) - if disk_should_exist: - if decon.result['changed']: - # If we arrive here, there is a good chance that the Disk is present - get fresh Disk - # facts by Disk ID. - # Otherwise, Disk facts from previous call (when the Disk was still in existence) will - # be returned. - _, disk_facts = decon.disk_find(disk_id) - decon.result['facts'] = decort_disk_package_facts(disk_facts, amodule.check_mode) - amodule.exit_json(**decon.result) + if amodule.params['name'] and amodule.params['id']: + if amodule.params['name'] != decon.disk_facts['name']: + decon.decort_disk_rename(amodule) + decon.result['changed'] = True + decon.result['msg'] = ("Disk with id '{}',renamed successfully from '{}' to '{}'.")\ + .format(decon.validated_disk_id, decon.disk_facts['name'], amodule.params['name']) + amodule.exit_json(**decon.result) if __name__ == "__main__": main() diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index bb378c4..cdc9ccf 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -785,7 +785,7 @@ class DecortController(object): Note: when Ansible is run in check mode method will return 0. """ - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "kvmvm_provision") + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "kvmvm_provision2") if self.amodule.check_mode: self.result['failed'] = False @@ -2051,8 +2051,27 @@ class DecortController(object): "response {}.").format(vins_id, api_resp.status_code, api_resp.reason) return ret_vins_id, ret_vins_dict + def _rg_listvins(self,rg_id): + """List all ViNS in the resource group + @param (int) rg_id: id onr resource group + """ + if not rg_id: + self.result['failed'] = True + self.result['msg'] = "_rg_listvins(): zero RG ID specified." + self.amodule.fail_json(**self.result) + + api_params = dict(rgId=rg_id, ) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/listVins", api_params) + if api_resp.status_code == 200: + ret_rg_vins_list = json.loads(api_resp.content.decode('utf8')) + else: + self.result['warning'] = ("rg_listvins(): failed to get RG by ID {}. HTTP code {}, " + "response {}.").format(rg_id, api_resp.status_code, api_resp.reason) + return [] - def vins_find(self, vins_id, vins_name="", account_id=0, rg_id=0, check_state=True): + return ret_rg_vins_list + + def vins_find(self, vins_id, vins_name="", account_id=0, rg_id=0, rg_facts="", check_state=True): """Find specified ViNS. @param (int) vins_id: ID of the ViNS. If non-zero vins_id is specified, all other arguments @@ -2093,29 +2112,29 @@ class DecortController(object): elif vins_name != "": if rg_id > 0: # search for ViNS at RG level - validated_id, validated_facts = self._rg_get_by_id(rg_id) - if not validated_id: - self.result['failed'] = True - self.result['msg'] = "vins_find(): cannot find RG ID {}.".format(rg_id) - self.amodule.fail_json(**self.result) - # NOTE: RG's 'vins' attribute does not list destroyed ViNSes! - for runner in validated_facts['vins']: - # api_params['vinsId'] = runner - ret_vins_id, ret_vins_facts = self._vins_get_by_id(runner) - if ret_vins_id and ret_vins_facts['name'] == vins_name: + # validated_id, validated_facts = self._rg_get_by_id(rg_id) + # if not validated_id: + # self.result['failed'] = True + # self.result['msg'] = "vins_find(): cannot find RG ID {}.".format(rg_id) + # self.amodule.fail_json(**self.result) + # # NOTE: RG's 'vins' attribute does not list destroyed ViNSes! + list_vins = self._rg_listvins(rg_id) + for vins in list_vins: + if vins['name'] == vins_name: + ret_vins_id, ret_vins_facts = self._vins_get_by_id(vins['id']) if not check_state or ret_vins_facts['status'] not in VINS_INVALID_STATES: return ret_vins_id, ret_vins_facts else: return 0, None elif account_id > 0: # search for ViNS at account level - validated_id, validated_facts = self.account_find("", account_id) - if not validated_id: - self.result['failed'] = True - self.result['msg'] = "vins_find(): cannot find Account ID {}.".format(account_id) - self.amodule.fail_json(**self.result) + # validated_id, validated_facts = self.account_find("", account_id) + # if not validated_id: + # self.result['failed'] = True + # self.result['msg'] = "vins_find(): cannot find Account ID {}.".format(account_id) + # self.amodule.fail_json(**self.result) # NOTE: account's 'vins' attribute does not list destroyed ViNSes! - for runner in validated_facts['vins']: + for runner in rg_facts['vins']: # api_params['vinsId'] = runner ret_vins_id, ret_vins_facts = self._vins_get_by_id(runner) if ret_vins_id and ret_vins_facts['name'] == vins_name: @@ -2296,7 +2315,7 @@ class DecortController(object): desired_state) return - def vins_update(self, vins_dict, ext_net_id, ext_ip_addr=""): + def vins_update_extnet(self, vins_dict, ext_net_id, ext_ip_addr=""): """Update ViNS. Currently only updates to the external network connection settings and external IP address assignment are implemented. Note that as ViNS created at account level cannot have external connections, attempt @@ -2318,7 +2337,7 @@ class DecortController(object): if self.amodule.check_mode: self.result['failed'] = False - self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' " + self.result['msg'] = ("vins_update_extnet() in check mode: updating ViNS ID {}, name '{}' " "was requested.").format(vins_dict['id'], vins_dict['name']) return @@ -2373,10 +2392,16 @@ class DecortController(object): gw_config[ 'ext_net_id']) return - def vins_update_mgmt(self, vins_dict, mgmtaddr=""): + def vins_update_mgmt(self, vins_dict, mgmtaddr=[]): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update_mgmt") + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("vins_update_mgmt() in check mode: updating ViNS ID {}, name '{}' " + "was requested.").format(vins_dict['id'], vins_dict['name']) + return + if self.amodule.params['config_save'] and vins_dict['VNFDev']['customPrecfg']: # only save config,no other modifictaion self.result['changed'] = True @@ -2386,13 +2411,12 @@ class DecortController(object): return for iface in vins_dict['VNFDev']['interfaces']: - if iface['ipAddress'] == mgmtaddr: - if not iface['listenSsh']: - self._vins_vnf_addmgmtaddr(vins_dict['VNFDev']['id'],mgmtaddr) - self.result['changed'] = True - self.result['failed'] = False - elif mgmtaddr =="": - if iface['listenSsh'] and iface['name'] != "ens9": + if iface['ipAddress'] in mgmtaddr and not iface['listenSsh']: + self._vins_vnf_addmgmtaddr(vins_dict['VNFDev']['id'],iface['ipAddress']) + self.result['changed'] = True + self.result['failed'] = False + elif iface['ipAddress'] not in mgmtaddr and iface['listenSsh']: + if iface['name'] != "ens9": self._vins_vnf_delmgmtaddr(vins_dict['VNFDev']['id'],iface['ipAddress']) self.result['changed'] = True self.result['failed'] = False @@ -2411,13 +2435,15 @@ class DecortController(object): def vins_update_ifaces(self,vins_dict,vinses=""): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update_ifaces") - existed_conn_ip = [] - #vnf_dict = self._get_vnf_by_id(vins_dict['VNFDev']['id']) - list_account_vins = self._get_all_account_vinses(vins_dict['VNFDev']['accountId']) - list_account_vinsid = [rec['id'] for rec in list_account_vins] - list_ifaces_ip = [rec['ipaddr'] for rec in vinses] - vins_inner = [rec['id'] for rec in vinses] - vins_outer = [rec['id'] for rec in list_account_vins] + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("vins_update_iface() in check mode: updating ViNS ID {}, name '{}' " + "was requested.").format(vins_dict['id'], vins_dict['name']) + return + + list_ifaces_ip = [rec['ipaddr'] for rec in vinses] + vinsid_not_existed = [] for iface in vins_dict['VNFDev']['interfaces']: if iface['connType'] == "VXLAN" and iface['type'] == "CUSTOM": if iface['ipAddress'] not in list_ifaces_ip: @@ -2425,19 +2451,30 @@ class DecortController(object): self.result['changed'] = True self.result['failed'] = False else: - existed_conn_ip.append(iface['ipAddress']) + #existed_conn_ip.append(iface['ipAddress']) + vinses = list(filter(lambda i: i['ipaddr']!=iface['ipAddress'],vinses)) + if not vinses: + return + list_account_vins = self._get_all_account_vinses(vins_dict['VNFDev']['accountId']) + list_account_vinsid = [rec['id'] for rec in list_account_vins] for vins in vinses: if vins['id'] in list_account_vinsid: _,v_dict = self._vins_get_by_id(vins['id']) - if vins['ipaddr'] not in existed_conn_ip: - self._vnf_iface_add(vins_dict['VNFDev']['id'],v_dict['vxlanId'],vins['ipaddr'],vins['netmask']) - self.result['changed'] = True - self.result['failed'] = False + #TODO: vins reservation + self._vnf_iface_add(vins_dict['VNFDev']['id'],v_dict['vxlanId'],vins['ipaddr'],vins['netmask']) + self.result['changed'] = True + self.result['failed'] = False + else: + vinsid_not_existed.append(vins['id']) + if vinsid_not_existed: + self.result['warning'] = ("List ViNS id: {} that not created on account id: {}").format( + vinsid_not_existed, + vins_dict['VNFDev']['accountId'] + ) return def _vnf_iface_add(self,arg_devid,arg_vxlanid,arg_ipaddr,arg_netmask="24",arg_defgw=""): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "_vnf_iface_add") api_params = dict( devId=arg_devid, ifType="CUSTOM", @@ -2473,7 +2510,6 @@ class DecortController(object): return ret_vnf_dict def _get_all_account_vinses(self,acc_id): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "get_all_account_vinses") api_params = dict(accountId=acc_id) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listVins", api_params) if api_resp.status_code == 200: @@ -2484,8 +2520,6 @@ class DecortController(object): return ret_listvins_dict def _vins_vnf_addmgmtaddr(self,dev_id,mgmtip): - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_addmgmtaddr") api_params = dict(devId=dev_id,ip=mgmtip) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/addMgmtAddr", api_params) if api_resp.status_code == 200: @@ -2497,8 +2531,6 @@ class DecortController(object): return def _vins_vnf_delmgmtaddr(self,dev_id,mgmtip): - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_delmgmtaddr") api_params = dict(devId=dev_id,ip=mgmtip) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/delMgmtAddr", api_params) if api_resp.status_code == 200: @@ -2510,7 +2542,6 @@ class DecortController(object): return def _vins_vnf_customconfig_set(self,dev_id,arg_mode=True): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_customconfig_set") api_params = dict(devId=dev_id,mode=arg_mode) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/customSet", api_params) if api_resp.status_code == 200: @@ -2522,7 +2553,6 @@ class DecortController(object): return def _vins_vnf_config_save(self,dev_id): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_config_save") api_params = dict(devId=dev_id) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/configSave", api_params) if api_resp.status_code == 200: @@ -2532,12 +2562,6 @@ class DecortController(object): self.result['warning'] = ("_vins_vnf_config_set(): failed to Save configuration on the VNF device. {}. HTTP code {}, " "response {}.").format(dev_id,api_resp.status_code, api_resp.reason) return - - def vins_vnf_ifaceadd(self): - return - - def vins_vnf_ifaceremove(self): - return ############################## # @@ -2545,7 +2569,7 @@ class DecortController(object): # ############################## - def disk_delete(self, disk_id, permanently=False, force_detach=False): + def disk_delete(self, disk_id, permanently, detach, reason): """Deletes specified Disk. @param (int) disk_id: ID of the Disk to be deleted. @@ -2562,8 +2586,9 @@ class DecortController(object): return api_params = dict(diskId=disk_id, - detach=force_detach, - permanently=permanently, ) + detach=detach, + permanently=permanently, + reason=reason) self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/delete", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. self.result['failed'] = False @@ -2602,7 +2627,7 @@ class DecortController(object): return ret_disk_id, ret_disk_dict - def disk_find(self, disk_id, disk_name="", account_id=0, check_state=False): + def disk_find(self, disk_id, name, account_id, check_state=False): """Find specified Disk. @param (int) disk_id: ID of the Disk. If non-zero disk_id is specified, all other arguments @@ -2638,11 +2663,11 @@ class DecortController(object): self.result['failed'] = True self.result['msg'] = "disk_find(): cannot find Disk by ID {}.".format(disk_id) self.amodule.fail_json(**self.result) - if not check_state or ret_disk_facts['status'] not in DISK_INVALID_STATES: + if not check_state or ret_disk_facts['status']: return ret_disk_id, ret_disk_facts else: return 0, None - elif disk_name != "": + elif name != "": if account_id > 0: api_params = dict(accountId=account_id) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/list", api_params) @@ -2650,15 +2675,15 @@ class DecortController(object): disks_list = json.loads(api_resp.content.decode('utf8')) for runner in disks_list: # return the first disk of the specified name that fulfills status matching rule - if runner['name'] == disk_name: - if not check_state or runner['status'] not in DISK_INVALID_STATES: + if runner['name'] == name: + if not check_state or runner['status']: return runner['id'], runner else: return 0, None else: # we are missing meaningful account_id - fail the module self.result['failed'] = True self.result['msg'] = ("disk_find(): cannot find Disk by name '{}' " - "when no account ID specified.").format(disk_name) + "when no account ID specified.").format(name) self.amodule.fail_json(**self.result) else: # Disk ID is 0 and Disk name is emtpy - fail the module self.result['failed'] = True @@ -2667,55 +2692,40 @@ class DecortController(object): return 0, None - def disk_provision(self, disk_name, size, account_id, sep_id, pool_name="", desc="", location=""): + def disk_create(self, accountId, gid, name, description, size, type, iops, sep_id, pool): """Provision Disk according to the specified arguments. Note that disks created by this method will be of type 'D' (data disks). If critical error occurs the embedded call to API function will abort further execution of the script and relay error to Ansible. - @param (string) disk_name: name to assign to the Disk. + @param (string) name: name to assign to the Disk. @param (int) size: size of the disk in GB. - @param (int) account_id: ID of the account where disk will belong. + @param (int) accountId: ID of the account where disk will belong. @param (int) sep_id: ID of the SEP (Storage Endpoint Provider), where disk will be created. @param (string) pool: optional name of the pool, where this disk will be created. - @param (string) desc: optional text description of this disk. - @param (string) location: optional location, where disk resources will be provided. If empty - string is specified the disk will be created in the default location under DECORT controller. + @param (string) description: optional text description of this disk. + @param (int) gid: optional Grid id, if specified the disk will be created in selected + location. @return: ID of the newly created Disk (in Ansible check mode 0 is returned). """ - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_provision") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "disk_provision() in check mode: create Disk name '{}' was requested.".format( - disk_name) - return 0 - - target_gid = self.gid_get(location) - if not target_gid: - self.result['failed'] = True - self.result['msg'] = "disk_provision() failed to obtain Grid ID for default location." - self.amodule.fail_json(**self.result) - - ret_disk_id = 0 - api_params = dict(accountId=account_id, - gid=target_gid, - name=disk_name, - desc=desc, + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_creation") + api_params = dict(accountId=accountId, + gid=gid, + name=name, + description=description, size=size, - type='D', - sepId=sep_id, ) - if pool_name != "": - api_params['pool'] = pool_name + type=type, + iops=iops, + sepId=sep_id, + pool=pool ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/create", api_params) if api_resp.status_code == 200: ret_disk_id = json.loads(api_resp.content.decode('utf8')) self.result['failed'] = False self.result['changed'] = True - return ret_disk_id def disk_resize(self, disk_facts, new_size): @@ -2770,6 +2780,48 @@ class DecortController(object): return + def disk_limitIO(self, limits, diskId): + """Limits already created Disk identified by its ID. + @param (dict) limits: Dictionary with limits. + @param (int) diskId: ID of the Disk to limit. + @returns: nothing on success. On error this method will abort module execution. + """ + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_limitIO") + api_params = dict(diskId=diskId, + total_bytes_sec=limits['total_bytes_sec'], + read_bytes_sec=limits['read_bytes_sec'], + write_bytes_sec=limits['write_bytes_sec'], + total_iops_sec=limits['total_iops_sec'], + read_iops_sec=limits['read_iops_sec'], + write_iops_sec=limits['write_iops_sec'], + total_bytes_sec_max=limits['total_bytes_sec_max'], + read_bytes_sec_max=limits['read_bytes_sec_max'], + write_bytes_sec_max=limits['write_bytes_sec_max'], + total_iops_sec_max=limits['total_iops_sec_max'], + read_iops_sec_max=limits['read_iops_sec_max'], + write_iops_sec_max=limits['write_iops_sec_max'], + size_iops_sec=limits['size_iops_sec']) + self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/limitIO", api_params) + self.result['msg'] = "Specified Disk ID {} limited successfully.".format(self.validated_disk_id) + return + + def disk_rename(self, diskId, name): + """Renames disk to the specified new name. + + @param disk_id: ID of the Disk to rename. + @param name: New name. + + @returns: nothing on success. On error this method will abort module execution. + """ + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_rename") + api_params = dict(diskId=diskId, + name=name) + self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/rename", api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + self.result['failed'] = False + self.result['changed'] = True + return + def disk_restore(self, disk_id): """Restores previously deleted Disk identified by its ID. For restore to succeed the Disk must be in 'DELETED' state. @@ -3146,7 +3198,7 @@ class DecortController(object): return api_params = dict(k8sId=k8s_id, - permanently=False, + permanently=permanently, ) self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/delete", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. @@ -3238,9 +3290,11 @@ class DecortController(object): ret_info = json.loads(api_get_resp.content.decode('utf8')) if api_get_resp.status_code == 200: if ret_info['status'] in ["PROCESSING", "SCHEDULED"]: + self.result['msg'] = ("k8s_provision(): Can't create cluster") self.result['failed'] = False time.sleep(30) elif ret_info['status'] == "ERROR": + self.result['msg'] = ("k8s_provision(): Can't create cluster") self.result['failed'] = True return elif ret_info['status'] == "OK": @@ -3250,10 +3304,13 @@ class DecortController(object): else: k8s_id = ret_info['status'] else: + self.result['msg'] = ("k8s_provision(): Can't create cluster") self.result['failed'] = True # Timeout + self.result['msg'] = ("k8s_provision(): Can't create cluster") self.result['failed'] = True else: + self.result['msg'] = ("k8s_provision(): Can't create cluster") self.result['failed'] = True return @@ -3283,15 +3340,16 @@ class DecortController(object): for rec_inn in arg_k8swg['k8sGroups']['workers']: for rec_out in arg_modwg: - if rec_inn['num'] != rec_out['num'] and rec_out['num'] != 0: - count = rec_inn['num']-rec_out['num'] - cmp_list = [] - if count > 0: - for cmp in rec_inn['detailedInfo'][:count]: - cmp_list.append(cmp['id']) - wg_moddel_list.append({rec_inn['id']:cmp_list}) - if count < 0: - wg_modadd_list.append({rec_inn['id']:abs(count)}) + if rec_inn['name'] == rec_out['name']: + if rec_inn['num'] != rec_out['num'] and rec_out['num'] != 0: + count = rec_inn['num']-rec_out['num'] + cmp_list = [] + if count > 0: + for cmp in rec_inn['detailedInfo'][-count:]: + cmp_list.append(cmp['id']) + wg_moddel_list.append({rec_inn['id']:cmp_list}) + if count < 0: + wg_modadd_list.append({rec_inn['id']:abs(count)}) if wg_del_list: for wgid in wg_del_list: @@ -3335,15 +3393,17 @@ class DecortController(object): api_params = dict(includeDisabled=False) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8ci/list", api_params) - + k8ci_id_present = False if api_resp.status_code == 200: ret_k8ci_list = json.loads(api_resp.content.decode('utf8')) for k8ci_item in ret_k8ci_list: if k8ci_item['id'] == arg_k8ci_id: + k8ci_id_present = True break - else: + + if k8ci_id_present == False: self.result['failed'] = True - self.result['msg'] = "k8s_k8ci_find(): cannot find ID." + self.result['msg'] = ("Cannot find k8ci id: {}.").format(arg_k8ci_id) self.amodule.fail_json(**self.result) else: self.result['failed'] = True