diff --git a/library/decort_rg.py b/library/decort_rg.py index 38b2d17..f273993 100644 --- a/library/decort_rg.py +++ b/library/decort_rg.py @@ -371,7 +371,6 @@ def main(): decon.rg_state(rg_facts, 'enabled') # TODO: Not sure what to do with the quotas after RG is restored. May need to update rg_facts. rg_should_exist = True - pass elif amodule.params['state'] == 'absent': # destroy permanently decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) @@ -390,7 +389,7 @@ def main(): if amodule.params['state'] in ('present', 'enabled'): # need to re-provision RG decon.check_amodule_argument('rg_name') - # As we alreafy have validated account ID we can create RG and get rg_id on success + # As we already have validated account ID we can create RG and get rg_id on success # pass empty string for location code, rg_provision will select the 1st location rg_id = decon.rg_provision(validated_acc_id, amodule.params['rg_name'], decon.decort_username, diff --git a/library/decort_vins.py b/library/decort_vins.py index 58eb832..9606ef6 100644 --- a/library/decort_vins.py +++ b/library/decort_vins.py @@ -80,8 +80,8 @@ options: required: yes ext_net_id: description: - - `Controls ViNS connection to an external network. This argument is optional with default value of -1, - which means no external connection.` + - 'Controls ViNS connection to an external network. This argument is optional with default value of -1, + which means no external connection.' - Specify 0 to connect ViNS to external network and let platform select external network Id automatically. - Specify positive value to request ViNS connection to the external network with corresponding ID. - You may also control external IP address selection with I(ext_ip_addr) argument. @@ -91,13 +91,21 @@ options: description: - IP address to assign to the external interface of this ViNS when connecting to the external net. - If empty string is passed, the platform will assign free IP address automatically. - - `Note that if invalid IP address or an address already occupied by another client is specified, - the module will abort with an error.` - - `This argument is used only for new connection to the specified network. You cannot select another + - 'Note that if invalid IP address or an address already occupied by another client is specified, + the module will abort with an error.' + - 'This argument is used only for new connection to the specified network. You cannot select another external IP address without changing external network ID.' - ViNS connection to the external network is controlled by I(ext_net_id) argument. default: empty string required: no + ipcidr: + description: + - Internal ViNS network address in a format XXX.XXX.XXX.XXX/XX (includes address and netmask). + - If empty string is passed, the platform will assign network address automatically. + - 'When selecting this address manually, note that this address must be unique amomng all ViNSes in + the target account.' + default: empty string + required: no jwt: description: - 'JWT (access token) for authenticating to the DECORT controller when I(authenticator=jwt).' @@ -172,6 +180,10 @@ options: scenario can lead to security issues, so please know what you are doing.' default: True required: no + vins_id: + description: + - ID of the ViNs to manage. If ViNS is identified by ID it must be present. + - If ViNS ID is specified, I(account_id), I(account_name), I(rg_id) and I(rg_name) are ignored. vins_name: description: - Name of the ViNS. @@ -219,6 +231,8 @@ facts: facts: id: 5 name: MyViNS + int_net_addr: 192.168.1.0 + ext_net_addr: 10.50.11.118 state: CREATED account_id: 7 rg_id: 19 @@ -253,11 +267,27 @@ def decort_vins_package_facts(arg_vins_facts, arg_check_mode=False): ret_dict['state'] = "ABSENT" return ret_dict - ret_dict['id'] = arg_rg_facts['id'] - ret_dict['name'] = arg_rg_facts['name'] - ret_dict['state'] = arg_rg_facts['status'] - ret_dict['account_id'] = arg_rg_facts['accountId'] - ret_dict['gid'] = arg_rg_facts['gid'] + ret_dict['id'] = arg_vins_facts['id'] + ret_dict['name'] = arg_vins_facts['name'] + ret_dict['state'] = arg_vins_facts['status'] + ret_dict['account_id'] = arg_vins_facts['accountId'] + ret_dict['rg_id'] = arg_vins_facts['rgid'] + ret_dict['int_net_addr'] = arg_vins_facts['network'] + ret_dict['gid'] = arg_vins_facts['gid'] + + if arg_vins_facts['vnfs'].get('GW'): + gw_config = arg_vins_facts['vnfs']['GW']['config'] + ret_dict['ext_ip_addr'] = gw_config['ext_net_ip'] + ret_dict['ext_net_id'] = gw_config['ext_net_id'] + else: + ret_dict['ext_ip_addr'] = "" + ret_dict['ext_net_id'] = -1 + + # arg_vins_facts['vnfs']['GW']['config'] + # ext_ip_addr -> ext_net_ip + # ??? -> ext_net_id + # tech_status -> techStatus + return ret_dict @@ -283,6 +313,7 @@ def decort_vins_parameters(): # datacenter=dict(type='str', required=False, default=''), ext_net_id=dict(type='int', required=False, default=-1), ext_ip_addr=dict(type='str', required=False, default=''), + ipcidr=dict(type='str', required=False, default=''), jwt=dict(type='str', required=False, fallback=(env_fallback, ['DECORT_JWT']), @@ -304,6 +335,7 @@ def decort_vins_parameters(): rg_id=dict(type='int', required=False, default=0), rg_name=dict(type='str', required=False, default=''), verify_ssl=dict(type='bool', required=False, default=True), + vins_id=dict(type='int', required=False, default=0), vins_name=dict(type='str', required=True), workflow_callback=dict(type='str', required=False), workflow_context=dict(type='str', required=False), @@ -334,150 +366,228 @@ def main(): decon = DecortController(amodule) - # We need valid Account ID to manage RG. - # Account may be specified either by account_id or account_name. In both cases we - # have to validate account presence and accesibility by the current user. + vins_id = 0 + vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level + vins_facts = None # will hold ViNS facts + validated_rg_id = 0 + rg_facts = None # will hold RG facts validated_acc_id = 0 - if decon.check_amodule_argument('account_id', False): - validated_acc_id, _ = decon.account_find("", amodule.params['account_id']) - else: - decon.check_amodule_argument('account_name') # if no account_name, this function will abort module - validated_acc_id, _ = decon.account_find(amodule.params['account_name']) + acc_facts = None # will hold Account facts - if not validated_acc_id: - # we failed to locate account by either name or ID - abort with an error + if amodule.params['vins_id']: + # expect existing ViNS with the specified ID + # This call to rg_vins will abort the module if no ViNS with such ID is present + vins_id, vins_facts = decon.vins_find(amodule.params['vins_id']) + if not vins_id: + decon.result['failed'] = True + decon.result['msg'] = "Specified ViNS ID {} not found.".format(amodule.params['vins_id']) + decon.fail_json(**decon.result) + vins_level="ID" + validated_acc_id = vins_facts['accountId'] + validated_rg_id = vins_facts['rgid'] + elif amodule.params['rg_id']: + # expect ViNS @ RG level in the RG with specified ID + vins_level="RG" + # This call to rg_find will abort the module if no RG with such ID is present + validated_rg_id, rg_facts = decon.rg_find(0, # account ID set to 0 as we search for RG by RG ID + amodule.params['rg_id'], arg_rg_name="") + # This call to vins_find may return vins_id=0 if no ViNS found + vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'], + account_id=0, + rg_id=amodule.params['rg_id'], + check_state=False) + # TODO: add checks and setup ViNS presence flags accordingly + pass + elif amodule.params['account_id'] or amodule.params['account_name'] != "": + # 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.") + decon.fail_json(**decon.result) + if amodule.params['rg_name'] != "": # at this point we know that rg_id=0 + # expect ViNS @ RG level in the RG with specified name under specified account + # RG with the specified name must be present under the account, otherwise abort the module + validated_rg_id, rg_facts = decon.rg_find(validated_acc_id, 0, amodule.params['rg_name']) + if (not validated_rg_id or + rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]): + decon.result['failed'] = True + decon.result['msg'] = "RG name '{}' not found or has invalid state.".format(amodule.params['rg_name']) + decon.fail_json(**decon.result) + # This call to vins_find may return vins_id=0 if no ViNS with this name found under specified RG + vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'], + account_id=0, # set to 0, as we are looking for ViNS under RG + rg_id=validated_rg_id, + check_state=False) + vins_level = "RG" + # TODO: add checks and setup ViNS presence flags accordingly + else: # At this point we know for sure that rg_name="" and rg_id=0 + # So we expect ViNS @ account level + # This call to vins_find may return vins_id=0 if no ViNS found + vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'], + account_id=validated_acc_id, + rg_id=0, + check_state=False) + vins_level = "ACC" + # TODO: add checks and setup ViNS presence flags accordingly + else: + # this is "invalid arguments combination" sink + # if we end up here, it means that module was invoked with vins_id=0 and rg_id=0 decon.result['failed'] = True - decon.result['msg'] = ("Current user does not have access to the requested account " - "or non-existent account specified.") + if amodule.params['account_id'] == 0 and amodule.params['account_name'] == "": + decon.result['msg'] = "Cannot find ViNS by name when account name is empty and account ID is 0." + if amodule.params['rg_name'] != "": + # rg_name without account specified + decon.result['msg'] = "Cannot find ViNS by name when RG name is empty and RG ID is 0." decon.fail_json(**decon.result) - # Check if the RG with the specified parameters already exists - rg_id, rg_facts = decon.rg_find(validated_acc_id, - 0, arg_rg_name=amodule.params['rg_name'], - arg_check_state=False) - rg_should_exist = True + # + # Initial validation of module arguments is complete + # + # At this point non-zero vins_id means that we will be managing pre-existing ViNS + # Otherwise we are about to create a new one as follows: + # - if validated_rg_id is non-zero, create ViNS @ RG level + # - if validated_rg_id is zero, create ViNS @ account level + # + # When managing existing ViNS we need to account for both "static" and "transient" + # status. Full range of ViNS statii is as follows: + # + # "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING" + # - if rg_id: - if rg_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]: - # error: nothing can be done to existing RG in the listed statii regardless of + vins_should_exist = False + + if vins_id: + vins_should_exist = True + if vins_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]: + # error: nothing can be done to existing ViNS 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 RG ID {} because of its current " - "status '{}'").format(rg_id, rg_facts['status']) - elif rg_facts['status'] == "DISABLED": + decon.result['msg'] = ("No change can be done for existing ViNS ID {} because of its current " + "status '{}'").format(vins_id, vins_facts['status']) + elif vins_facts['status'] == "DISABLED": if amodule.params['state'] == 'absent': - decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) - rg_facts['status'] = 'DESTROYED' - rg_should_exist = False + decon.vins_delete(vins_id, permanently=True) + vins_facts['status'] = 'DESTROYED' + vins_should_exist = False elif amodule.params['state'] in ('present', 'disabled'): - # update quotas - decon.rg_quotas(rg_facts, amodule.params['quotas']) + # update ViNS, leave in disabled state + decon.vins_update(vins_facts, + amodule.params['ext_net_id'], amodule.params['ext_ip_addr']) elif amodule.params['state'] == 'enabled': - # update quotas and enable - decon.rg_quotas(rg_facts, amodule.params['quotas']) - decon.rg_state(rg_facts, 'enabled') - elif rg_facts['status'] == "CREATED": + # update ViNS and enable + decon.vins_update(vins_facts, + amodule.params['ext_net_id'], amodule.params['ext_ip_addr']) + decon.vins_state(vins_facts, 'enabled') + elif vins_facts['status'] in ["CREATED", "ENABLED"]: if amodule.params['state'] == 'absent': - decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) - rg_facts['status'] = 'DESTROYED' - rg_should_exist = False + decon.vins_delete(vins_id, permanently=True) + vins_facts['status'] = 'DESTROYED' + vins_should_exist = False elif amodule.params['state'] in ('present', 'enabled'): - # update quotas - decon.rg_quotas(rg_facts, amodule.params['quotas']) + # update ViNS + decon.vins_update(vins_facts, + amodule.params['ext_net_id'], amodule.params['ext_ip_addr']) elif amodule.params['state'] == 'disabled': - # disable and update quotas - decon.rg_state(rg_facts, 'disabled') - decon.rg_quotas(rg_facts, amodule.params['quotas']) - elif rg_facts['status'] == "DELETED": + # disable and update ViNS + decon.vins_state(vins_facts, 'disabled') + decon.vins_update(vins_facts, + amodule.params['ext_net_id'], amodule.params['ext_ip_addr']) + elif vins_facts['status'] == "DELETED": if amodule.params['state'] in ['present', 'enabled']: # restore and enable - # TODO: check if restore RG API returns the new RG ID of the restored RG instance. - decon.rg_restore(arg_rg_id=rg_id) - decon.rg_state(rg_facts, 'enabled') - # TODO: Not sure what to do with the quotas after RG is restored. May need to update rg_facts. - rg_should_exist = True - pass + decon.vins_restore(arg_vins_id=vins_id) + decon.vins_state(vins_facts, 'enabled') + vins_should_exist = True elif amodule.params['state'] == 'absent': # destroy permanently - decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True) - rg_facts['status'] = 'DESTROYED' - rg_should_exist = False + decon.vins_delete(vins_id, permanently=True) + vins_facts['status'] = 'DESTROYED' + vins_should_exist = False elif amodule.params['state'] == 'disabled': # error decon.result['failed'] = True decon.result['changed'] = False - decon.result['msg'] = ("Invalid target state '{}' requested for RG ID {} in the " - "current status '{}'").format(rg_id, + decon.result['msg'] = ("Invalid target state '{}' requested for ViNS ID {} in the " + "current status '{}'").format(vins_id, amodule.params['state'], - rg_facts['status']) - rg_should_exist = False - elif rg_facts['status'] == "DESTROYED": + vins_facts['status']) + vins_should_exist = False + elif vins_facts['status'] == "DESTROYED": if amodule.params['state'] in ('present', 'enabled'): - # need to re-provision RG - decon.check_amodule_argument('rg_name') - # As we alreafy have validated account ID we can create RG and get rg_id on success - # pass empty string for location code, rg_provision will select the 1st location - rg_id = decon.rg_provision(validated_acc_id, - amodule.params['rg_name'], decon.decort_username, - amodule.params['quotas']) - rg_should_exist = True + # need to re-provision ViNS; some attributes may be changed, some stay the same. + # account and RG - stays the same + # vins_name - stays the same + # IPcidr - take from module arguments + # ext IP address - take from module arguments + # annotation - take from module arguments + vins_id = decon.vins_provision(vins_facts['name'], + validated_acc_id, validated_rg_id, + amodule.params['ipcidr'], + amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], + amodule.params['annotation']) + vins_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 RG ID {} because of its " - "current status '{}'").format(rg_id, - rg_facts['status']) - rg_should_exist = False + decon.result['msg'] = ("No state change required for ViNS ID {} because of its " + "current status '{}'").format(vins_id, + vins_facts['status']) + vins_should_exist = False elif amodule.params['state'] == 'disabled': # error decon.result['failed'] = True decon.result['changed'] = False - decon.result['msg'] = ("Invalid target state '{}' requested for RG ID {} in the " - "current status '{}'").format(rg_id, + decon.result['msg'] = ("Invalid target state '{}' requested for ViNS ID {} in the " + "current status '{}'").format(vins_id, amodule.params['state'], - rg_facts['status']) + vins_facts['status']) else: - # Preexisting RG was not found. - rg_should_exist = False # we will change it back to True if RG is explicitly created or restored + # Preexisting ViNS was not found. + vins_should_exist = False # we will change it back to True if ViNS is created or restored # If requested state is 'absent' - nothing to do if 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 RG name '{}'").format(amodule.params['rg_name']) + "non-existent ViNS name '{}'").format(amodule.params['vins_name']) elif amodule.params['state'] in ('present', 'enabled'): # Target RG does not exist yet - create it and store the returned ID in rg_id variable for later use # To create RG we need account name (or account ID) and RG name - check # that these parameters are present and proceed. - decon.check_amodule_argument('rg_name') - # as we already have account ID we can create RG and get rg_id on success - # pass empty string for location code, rg_provision will select the 1st location - rg_id = decon.rg_provision(validated_acc_id, - amodule.params['rg_name'], decon.decort_username, - amodule.params['quotas']) - rg_should_exist = True + decon.check_amodule_argument('vins_name') + # as we already have account ID and RG ID we can create ViNS and get vins_id on success + vins_id = decon.vins_provision(amodule.params['vins_name'], + validated_acc_id, validated_rg_id, + amodule.params['ipcidr'], + amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], + amodule.params['annotation']) + vins_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 " - "RG name '{}' ").format(amodule.params['state'], - amodule.params['rg_name']) + "ViNS name '{}'").format(amodule.params['state'], + amodule.params['vins_name']) + # # conditional switch end - complete module run + # if decon.result['failed']: amodule.fail_json(**decon.result) else: - # prepare RG facts to be returned as part of decon.result and then call exit_json(...) - # rg_facts = None - if rg_should_exist: + # prepare ViNS facts to be returned as part of decon.result and then call exit_json(...) + if vins_should_exist: if decon.result['changed']: - # If we arrive here, there is a good chance that the RG is present - get fresh RG facts from - # the cloud by RG ID. - # Otherwise, RG facts from previous call (when the RG was still in existence) will be returned. - _, rg_facts = decon.rg_find(arg_account_id=0, arg_rg_id=rg_id) - decon.result['facts'] = decort_rg_package_facts(rg_facts, amodule.check_mode) + # If we arrive here, there is a good chance that the ViNS is present - get fresh ViNS + # facts from # the cloud by ViNS ID. + # Otherwise, ViNS facts from previous call (when the ViNS was still in existence) will + # be returned. + _, vins_facts = decon.vins_find(vins_id) + decon.result['facts'] = decort_vins_package_facts(vins_facts, amodule.check_mode) amodule.exit_json(**decon.result) diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index 92ae126..772eca2 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -1257,6 +1257,11 @@ class DecortController(object): ret_rg_id = 0 ret_rg_dict = dict() + if not arg_rg_id: + self.result['failed'] = True + self.result['msg'] = "rg_get_by_id(): zero RG ID specified." + self.amodule.fail_json(**self.result) + api_params = dict(rgId=arg_rg_id,) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/get", api_params) if api_resp.status_code == 200: @@ -1275,12 +1280,13 @@ class DecortController(object): However, it does fail the run if RG cannot be located by arg_rg_id (if non zero specified) or if API errors occur. - @param (int) arg_account_id: ID of the account where to look for the RG. + @param (int) arg_account_id: ID of the account where to look for the RG. Set to 0 if RG is to be located by + its ID. @param (int) arg_rg_id: integer ID of the RG to be found. If non-zero RG ID is passed, account ID and RG name are ignored. However, RG must be present in this case, as knowing its ID implies it already exists, otherwise method will fail. @param (string) arg_rg_name: string that defines the name of RG to be found. This parameter is case sensitive. - @param (bool) arg_check_state: boolean that tells the method to report RGs in valid states only. + @param (bool) arg_check_state: tells the method to report RGs in valid states only. @return: ID of the RG, if found. Zero otherwise. @return: dictionary with RG facts if RG is present. Empty dictionary otherwise. None on error. @@ -1289,7 +1295,7 @@ class DecortController(object): # Resource group can be in one of the following states: # MODELED, CREATED, DISABLING, DISABLED, ENABLING, DELETING, DELETED, DESTROYED, DESTROYED # - # Transient state (ending with ING) are invalid from RG manipularion viewpoint + # Transient state (ending with ING) are invalid from RG manipulation viewpoint # RG_INVALID_STATES = ["MODELED"] @@ -1317,7 +1323,7 @@ class DecortController(object): self.result['failed'] = True self.result['msg'] = "rg_find(): cannot find RG by name if account ID is zero or less." self.amodule.fail_json(**self.result) - # try to locate RG by name - start with gettin all RGs IDs within the specified account + # try to locate RG by name - start with getting all RGs IDs within the specified account api_params['accountId'] = arg_account_id api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params) if api_resp.status_code == 200: @@ -1416,7 +1422,7 @@ class DecortController(object): api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/create", api_params) # On success the above call will return here. On error it will abort execution by calling fail_json. - # API /restmachine/cloudapi/cloudspaces/create returns ID of the newly created VDC on success + # API /restmachine/cloudapi/rg/create returns ID of the newly created RG on success self.result['failed'] = False self.result['changed'] = True ret_rg_id = int(api_resp.content.decode('utf8')) @@ -1510,8 +1516,8 @@ class DecortController(object): def rg_state(self, arg_rg_dict, arg_desired_state): """Enable or disable RG. - @param arg_rg_dict: dictionary with the target VDC facts as returned by vdc_find(...) method or - .../rg/get API call to obtain the data. + @param arg_rg_dict: dictionary with the target RG facts as returned by rg_find(...) method or + .../rg/get API call. @param arg_desired_state: the desired state for this RG. Valid states are 'enabled' and 'disabled'. """ @@ -1617,20 +1623,22 @@ class DecortController(object): api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params) if api_resp.status_code == 200: account_details = json.loads(api_resp.content.decode('utf8')) - account_record = { - 'id': account_details['id'], - 'name': account_details['name'], - } - return account_details['id'], account_record + return account_details['id'], account_details else: api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/list", api_params) if api_resp.status_code == 200: # Parse response to see if a account matching arg_account_name is found in the output # If it is found, assign its ID to the return variable and copy dictionary with the facts accounts_list = json.loads(api_resp.content.decode('utf8')) - for account_record in accounts_list: - if account_record['name'] == arg_account_name: - return account_record['id'], account_record + for runner in accounts_list: + if runner['name'] == arg_account_name: + # get detailed information about the account from "accounts/get" call as + # "accounts/list" does not return all necessary fields + api_params['accountId'] = runner['id'] + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params) + if api_resp.status_code == 200: + account_details = json.loads(api_resp.content.decode('utf8')) + return account_details['id'], account_details return 0, None @@ -1790,3 +1798,390 @@ class DecortController(object): return ret_gid + # + # ViNS management + # + + def vins_delete(self, vins_id, permanently=False): + """Deletes specified ViNS. + + @param (int) vins_id: integer value that identifies the ViNS to be deleted. + @param (bool) arg_permanently: a bool that tells if deletion should be permanent. If False, the ViNS will be + marked as DELETED and placed into a trash bin for predefined period of time (usually, a few days). Until + this period passes this ViNS can be restored by calling the corresponding 'restore' method. + """ + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_delete") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['changed'] = False + self.result['msg'] = "vins_delete() in check mode: delete ViNS ID {} was requested.".format(vins_id) + return + + # + # TODO: need decision if deleting a VINS with connected computes is allowed (aka force=True) + # and implement this decision accordingly. + # + + api_params = dict(vinsId=vins_id, + # force=True | False, + permanently=permanently,) + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/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 + self.result['changed'] = True + return + + def _vins_get_by_id(self, vins_id): + """Helper function that locates ViNS by ID and returns ViNS facts. This function + expects that the ViNS exists (albeit in DELETED or DESTROYED state) and will return + 0 ViNS ID if not found. + + @param (int) vins_id: ID of the ViNS to find and return facts for. + + @return: ViNS ID and a dictionary of ViNS facts as provided by vins/get API call. + + Note that if it fails to find the ViNS with the specified ID, it may return 0 for ID + and empty dictionary for the facts. So it is suggested to check the return values + accordingly in the upstream code. + """ + ret_vins_id = 0 + ret_vins_dict = dict() + + if not vins_id: + self.result['failed'] = True + self.result['msg'] = "vins_get_by_id(): zero ViNS ID specified." + self.amodule.fail_json(**self.result) + + api_params = dict(vinsId=vins_id,) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/get", api_params) + if api_resp.status_code == 200: + ret_vins_id = vins_id + ret_vins_dict = json.loads(api_resp.content.decode('utf8')) + else: + self.result['warning'] = ("vins_get_by_id(): failed to get VINS by ID {}. HTTP code {}, " + "response {}.").format(vins_id, api_resp.status_code, api_resp.reason) + + return ret_vins_id, ret_vins_dict + + def vins_find(self, vins_id, vins_name="", account_id=0, rg_id=0, check_state=True): + """Find specified ViNS. + + @param (int) vins_id: ID of the ViNS. If non-zero vins_id is specified, all other arguments + are ignored, ViNS must exist and is located by its ID only. + @param (string) vins_name: If vins_id is 0, then vins_name is mandatory. Further search for + ViNS is based on combination of account_id and rg_id. If account_id is non-zero, then rg_id + is ignored and ViNS is supposed to exist at account level. + @param (int) account_id: set to non-zero value to search for ViNS by name at this account level. + @param (int) rg_id: set to non-zero value to search for ViNS by name at this RG level. Note, that + in this case account_id should be set to 0. + @param (bool) check_state: tells the method to report ViNSes in valid states only. Set check_state + to False if you want to check if specified ViNS exists at all without failing the module execution. + + @returns: ViNS ID and dictionary with ViNS facts. It may return zero ID and empty dictionary + if no ViNS found and check_state=False, so make sure to check return values in the upstream + code accordingly. + """ + + # transient and deleted/destroyed states are deemed invalid + VINS_INVALID_STATES = ["ENABLING", "DISABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"] + + ret_vins_id = 0 + # api_params = dict() + ret_vins_facts = None + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_find") + + if vins_id > 0: + ret_vins_id, ret_vins_facts = self._vins_get_by_id(vins_id) + if not ret_vins_id: + self.result['failed'] = True + self.result['msg'] = "vins_find(): cannot find ViNS by ID {}.".format(vins_id) + self.amodule.fail_json(**self.result) + 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 vins_name != "": + if account_id > 0: + # ignore rg_id and 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) + # TODO: account's 'vins' attribute does not list deleted or 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: + 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 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) + # TODO: RG's 'vins' attribute does not list deleted or 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: + 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 + else: # both Account ID and RG ID are zero - fail the module + self.result['failed'] = True + self.result['msg'] = ("vins_find(): cannot find ViNS by name '{}' " + "when no account ID or RG ID is specified.").format(vins_name) + self.amodule.fail_json(**self.result) + else: # ViNS ID is 0 and ViNS name is emtpy - fail the module + self.result['failed'] = True + self.result['msg'] = "vins_find(): cannot find ViNS with zero ID and empty name." + self.amodule.fail_json(**self.result) + + return 0, None + + def vins_provision(self, vins_name, account_id, rg_id=0, ipcidr="", ext_net_id=-1, ext_ip_addr="", desc=""): + """Provision ViNS according to the specified arguments. + If critical error occurs the embedded call to API function will abort further execution of the script + and relay error to Ansible. + Note, that when creating ViNS at account level, default location under DECORT controller will be + selected automatically. + + @param (int) account_id: ID of the account where ViNS will be created. To create ViNS at account + level specify non-zero account ID and zero RG ID. + @param (string) rg_id: ID of the RG where ViNS will be created. If non-zero RG ID is specified, + ViNS will be created at this RG level. + @param (string) ipcidr: optional IP network address to use for internal ViNS network. + @param (int) ext_net_id: optional ID of the external network to connect this ViNS to. Specify -1 + to created isolated ViNS, 0 to let platform select default external network, or ID of the network + to ust. Note: this parameter is ignored for ViNS created at account level. + @param (string) ext_ip_addr: optional IP address of the external network connection for this ViNS. If + emtpy string is passed when ext_net_id >= 0, the platform will assign IP address automatically. If + explicitly specified IP address is invalid or already occupied, the method will fail. Note: this + parameter is ignored for ViNS created at account level. + @param (string) desc: optional text description of this ViNS. + + @return: ID of the newly created ViNS (in Ansible check mode 0 is returned). + """ + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_provision") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['changed'] = False + self.result['msg'] = ("vins_provision() in check mode: provision ViNS name '{}' was " + "requested.").format(vins_name) + return 0 + + if vins_name == "": + self.result['failed'] = True + self.result['msg'] = "vins_provision(): ViNS name cannot be empty." + self.amodule.fail_json(**self.result) + + api_url = "" + api_params = None + if account_id and not rg_id: + api_url = "/restmachine/cloudapi/vins/createInAccount" + target_gid = self.gid_get("") + if not target_gid: + self.result['failed'] = True + self.result['msg'] = "vins_provision() failed to obtain Grid ID for default location." + self.amodule.fail_json(**self.result) + api_params = dict( + name=vins_name, + accountId=account_id, + gid=target_gid, + ) + elif rg_id: + api_url = "/restmachine/cloudapi/vins/createInRG" + api_params = dict( + name=vins_name, + rgId=rg_id, + extNetId=ext_net_id, + ) + if ext_ip_addr: + api_params['extIp'] = ext_ip_addr + else: + self.result['failed'] = True + self.result['msg'] = "vins_provision(): either account ID or RG ID must be specified." + self.amodule.fail_json(**self.result) + + if ipcidr != "": + api_params['ipcidr'] = ipcidr + api_params['desc'] = desc + + api_resp = self.decort_api_call(requests.post, api_url, api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + # API /restmachine/cloudapi/vins/create*** returns ID of the newly created ViNS on success + self.result['failed'] = False + self.result['changed'] = True + ret_vins_id = int(api_resp.content.decode('utf8')) + return ret_vins_id + + def vins_restore(self, vins_id): + """Restores previously deleted ViNS identified by its ID. For restore to succeed + the ViNS must be in 'DELETED' state. + + @param vins_id: ID of the ViNS to restore. + + @returns: nothing on success. On error this method will abort module execution. + """ + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_restore") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['changed'] = False + self.result['msg'] = "vins_restore() in check mode: restore ViNS ID {} was requested.".format(vins_id) + return + + api_params = dict(vinsId=vins_id, + reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/restore", 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 vins_state(self, vins_dict, desired_state): + """Enable or disable ViNS. + + @param vins_dict: dictionary with the target ViNS facts as returned by vins_find(...) method or + .../vins/get API call. + @param desired_state: the desired state for this ViNS. Valid states are 'enabled' and 'disabled'. + """ + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_state") + + NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"] + VALID_TARGET_STATES = ["enabled", "disabled"] + + if vins_dict['status'] in NOP_STATES_FOR_VINS_CHANGE: + self.result['failed'] = False + self.result['msg'] = ("vins_state(): no state change possible for ViNS ID {} " + "in its current state '{}'.").format(vins_dict['id'], vins_dict['status']) + return + + if desired_state not in VALID_TARGET_STATES: + self.result['failed'] = False + self.result['warning'] = ("vins_state(): unrecognized desired state '{}' requested " + "for ViNS ID {}. No ViNS state change will be done.").format(desired_state, + vins_dict['id']) + return + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['changed'] = False + self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to " + "'{}' was requested.").format(vins_dict['id'],vins_dict['name'], + desired_state) + return + + vinsstate_api = "" # this string will also be used as a flag to indicate that API call is necessary + api_params = dict(vinsId=vins_dict['id'], + reason='Changed by DECORT Ansible module, vins_state method.') + + if vins_dict['status'] in ["CREATED", "ENABLED"] and desired_state == 'disabled': + rgstate_api = "/restmachine/cloudapi/vins/disable" + elif vins_dict['status'] == "DISABLED" and desired_state == 'enabled': + rgstate_api = "/restmachine/cloudapi/vins/enable" + + if vinsstate_api != "": + self.decort_api_call(requests.post, vinsstate_api, 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 + else: + self.result['failed'] = False + self.result['msg'] = ("vins_state(): no state change required for ViNS ID {} from current " + "state '{}' to desired state '{}'.").format(vins_dict['id'], + vins_dict['status'], + desired_state) + return + + def vins_update(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 + to update such ViNS will have no effect. + + @param (dict) vins_dict: dictionary with target ViNS details as returned by vins_find() method. + @param (int) ext_net_id: sets ViNS network connection status. Pass -1 to disconnect ViNS from + external network or positive network ID to connect to the specified external network. + @param (string) ext_ip_addr: optional IP address to assign to the external network connection + of this ViNS. + """ + + api_params = dict(vinsId=vins_dict['id'],) + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['changed'] = False + self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' " + "was requested.").format(vins_dict['id'],vins_dict['name']) + return + + if not vins_dict['rgid']: + # this ViNS exists at account level - no updates are possible + self.result['warning'] = ("vins_update(): no update is possible for ViNS ID {} " + "as it exists at account level.").format(vins_dict['id']) + return + + gw_config = None + if vins_dict['vnfs'].get('GW'): + gw_config = vins_dict['vnfs']['GW']['config'] + + if ext_net_id < 0: + # Request to have ViNS disconnected from external network + if gw_config: + # ViNS is connected to external network indeed - call API to disconnect; otherwise - nothing to do + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetDisconnect", api_params) + self.result['failed'] = False + self.result['changed'] = True + # On success the above call will return here. On error it will abort execution by calling fail_json. + elif ext_net_id > 0: + if gw_config: + # Request to have ViNS connected to the specified external network + # First check that if we are not connected to the same network already; otherwise - nothing to do + if gw_config['ext_net_id'] != ext_net_id: + # disconnect from current, we already have vinsId in the api_params + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetDisconnect", api_params) + self.result['changed'] = True + # connect to the new + api_params['netId'] = ext_net_id + api_params['ip'] = ext_ip_addr + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetConnect", api_params) + self.result['failed'] = False + # On success the above call will return here. On error it will abort execution by calling fail_json. + else: + self.result['changed'] = False + self.result['failed'] = False + self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " + "ignore ext IP address change if any.").format(vins_dict['id'], + ext_net_id) + else: # ext_net_id = 0, i.e. connect ViNS to default network + # we will connect ViNS to default network only if it is NOT connected to any ext network yet + if not gw_config: + api_params['netId'] = 0 + self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetConnect", api_params) + self.result['changed'] = True + self.result['failed'] = False + else: + self.result['changed'] = False + self.result['failed'] = False + self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " + "no reconnection to default network will be done.").format(vins_dict['id'], + gw_config['ext_net_id']) + + return +