from osmclient.common.exceptions import ClientException, NotFound
import json
-from time import sleep
-import sys
+from time import sleep, time
+from sys import stderr
# Declare a constant for each module, to allow customizing each timeout in the future
TIMEOUT_GENERIC_OPERATION = 600
POLLING_TIME_INTERVAL = 5
MAX_DELETE_ATTEMPTS = 3
+
def _show_detailed_status(old_detailed_status, new_detailed_status):
if new_detailed_status is not None and new_detailed_status != old_detailed_status:
- sys.stderr.write("detailed-status: {}\n".format(new_detailed_status))
+ stderr.write("detailed-status: {}\n".format(new_detailed_status))
return new_detailed_status
else:
return old_detailed_status
+
def _get_finished_states(entity):
- # Note that the member name is either:
- # 'operationState' (NS, NSI)
- # '_admin.'operationalState' (VIM, WIM, SDN)
- # For NS and NSI, 'operationState' may be one of:
- # PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK
- # For VIM, WIM, SDN: '_admin.operationalState' may be one of:
- # operationalState: ENABLED, DISABLED, ERROR, PROCESSING
+ """
+ Member name is either:
+ operationState' (NS, NSI)
+ '_admin.'operationalState' (VIM, WIM, SDN)
+ For NS and NSI, 'operationState' may be one of:
+ PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK
+ For VIM, WIM, SDN: '_admin.operationalState' may be one of:
+ ENABLED, DISABLED, ERROR, PROCESSING
+
+ :param entity: can be NS, NSI, or other
+ :return: two tuples with status completed strings, status failed string
+ """
if entity == 'NS' or entity == 'NSI':
- return ['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED_TEMP', 'FAILED']
+ return ('COMPLETED', 'PARTIALLY_COMPLETED'), ('FAILED_TEMP', 'FAILED')
else:
- return ['ENABLED', 'ERROR']
+ return ('ENABLED', ), ('ERROR', )
+
def _get_operational_state(resp, entity):
- # Note that the member name is either:
- # 'operationState' (NS)
- # 'operational-status' (NSI)
- # '_admin.'operationalState' (other)
+ """
+ The member name is either:
+ 'operationState' (NS)
+ 'operational-status' (NSI)
+ '_admin.'operationalState' (other)
+ :param resp: descriptor of the get response
+ :param entity: can be NS, NSI, or other
+ :return: status of the operation
+ """
if entity == 'NS' or entity == 'NSI':
return resp.get('operationState')
else:
return resp.get('_admin', {}).get('operationalState')
+
def _op_has_finished(resp, entity):
- # This function returns:
- # 0 on success (operation has finished)
- # 1 on pending (operation has not finished)
- # -1 on error (bad response)
- #
- finished_states = _get_finished_states(entity)
+ """
+ Indicates if operation has finished ok or is processing
+ :param resp: descriptor of the get response
+ :param entity: can be NS, NSI, or other
+ :return:
+ True on success (operation has finished)
+ False on pending (operation has not finished)
+ raise Exception if unexpected response, or ended with error
+ """
+ finished_states_ok, finished_states_error = _get_finished_states(entity)
if resp:
- operationalState = _get_operational_state(resp, entity)
- if operationalState:
- if operationalState in finished_states:
- return 0
- return 1
- return -1
-
-def _get_detailed_status(resp, entity, detailed_status_deleted):
- if detailed_status_deleted:
- return detailed_status_deleted
- if entity == 'NS' or entity == 'NSI':
+ op_state = _get_operational_state(resp, entity)
+ if op_state:
+ if op_state in finished_states_ok:
+ return True
+ elif op_state in finished_states_error:
+ raise ClientException("Operation failed with status '{}'".format(op_state))
+ return False
+ raise ClientException('Unexpected response from server: {} '.format(resp))
+
+
+def _get_detailed_status(resp, entity):
+ """
+ For VIM, WIM, SDN, 'detailed-status' is either:
+ - a leaf node to '_admin' (operations NOT supported)
+ - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI)
+ :param resp: content of the get response
+ :param entity: can be NS, NSI, or other
+ :return:
+ """
+ if entity in ('NS', 'NSI'):
# For NS and NSI, 'detailed-status' is a JSON "root" member:
return resp.get('detailed-status')
else:
- # For VIM, WIM, SDN, 'detailed-status' is either:
- # - a leaf node to '_admin' (operations NOT supported)
- # - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI)
- # https://osm.etsi.org/gerrit/#/c/7767 : LCM support for operations
- # https://osm.etsi.org/gerrit/#/c/7734 : NBI support for current_operation
ops = resp.get('_admin', {}).get('operations')
- op_index = resp.get('_admin', {}).get('current_operation')
- if ops and op_index:
+ current_op = resp.get('_admin', {}).get('current_operation')
+ if ops and current_op is not None:
# Operations are supported, verify operation index
- if isinstance(op_index, (int)) or op_index.isdigit():
- op_index = int(op_index)
- if op_index > 0 and op_index < len(ops) and ops[op_index] and ops[op_index]["detailed-status"]:
- return ops[op_index]["detailed-status"]
+ if isinstance(ops, dict) and current_op in ops:
+ return ops[current_op].get("detailed-status")
+ elif isinstance(ops, list) and isinstance(current_op, int) or current_op.isdigit():
+ current_op = int(current_op)
+ if current_op >= 0 and current_op < len(ops) and ops[current_op] and ops[current_op]["detailed-status"]:
+ return ops[current_op]["detailed-status"]
# operation index is either non-numeric or out-of-range
return 'Unexpected error when getting detailed-status!'
else:
# Operations are NOT supported
return resp.get('_admin', {}).get('detailed-status')
-def _has_delete_error(resp, entity, deleteFlag, delete_attempts_left):
- if deleteFlag and delete_attempts_left:
- state = _get_operational_state(resp, entity)
- if state and state == 'ERROR':
- return True
- return False
def wait_for_status(entity_label, entity_id, timeout, apiUrlStatus, http_cmd, deleteFlag=False):
- # Arguments:
- # entity_label: String describing the entities using '--wait':
- # 'NS', 'NSI', 'SDNC', 'VIM', 'WIM'
- # entity_id: The ID for an existing entity, the operation ID for an entity to create.
- # timeout: See section at top of this file for each value of TIMEOUT_<ENTITY>_OPERATION
- # apiUrlStatus: The endpoint to get the Response including 'detailed-status'
- # http_cmd: callback to HTTP command.
- # Passing this callback as an argument avoids importing the 'http' module here.
+ """
+ Wait until operation ends, making polling every 5s. Prints detailed status when it changes
+ :param entity_label: String describing the entities using '--wait': 'NS', 'NSI', 'SDNC', 'VIM', 'WIM'
+ :param entity_id: The ID for an existing entity, the operation ID for an entity to create.
+ :param timeout: Timeout in seconds
+ :param apiUrlStatus: The endpoint to get the Response including 'detailed-status'
+ :param http_cmd: callback to HTTP command. (Normally the get method)
+ :param deleteFlag: If this is a delete operation
+ :return: None, exception if operation fails or timeout
+ """
# Loop here until the operation finishes, or a timeout occurs.
- time_left = timeout
+ time_to_finish = time() + timeout
detailed_status = None
- detailed_status_deleted = None
- time_to_return = False
- delete_attempts_left = MAX_DELETE_ATTEMPTS
- wait_for_404 = False
- try:
- while True:
+ retries = 0
+ max_retries = 1
+ while True:
+ try:
http_code, resp_unicode = http_cmd('{}/{}'.format(apiUrlStatus, entity_id))
- resp = ''
- if resp_unicode:
- resp = json.loads(resp_unicode)
- # print('HTTP CODE: {}'.format(http_code))
- # print('RESP: {}'.format(resp))
- # print('URL: {}/{}'.format(apiUrlStatus, entity_id))
- if deleteFlag and http_code == 404:
- # In case of deletion, '404 Not Found' means successfully deleted
- # Display 'detailed-status: Deleted' and return
- time_to_return = True
- detailed_status_deleted = 'Deleted'
- elif deleteFlag and http_code in (200, 201, 202, 204):
- # In case of deletion and HTTP Status = 20* OK, deletion may be PROCESSING or COMPLETED
- # If this is the case, we should keep on polling until 404 (deleted) is returned.
- wait_for_404 = True
- elif http_code not in (200, 201, 202, 204):
- raise ClientException(str(resp))
- if not time_to_return:
- # Get operation status
- op_status = _op_has_finished(resp, entity_label)
- if op_status == -1:
- # An error occurred
- raise ClientException('unexpected response from server - {} '.format(
- str(resp)))
- elif op_status == 0:
- # If there was an error upon deletion, try again to delete the same instance
- # If the error is the same, there is probably nothing we can do but exit with error.
- # If the error is different (i.e. 404), the instance was probably already corrupt, that is,
- # operation(al)State was probably ERROR before deletion.
- # In such a case, even if the previous state was ERROR, the deletion was successful,
- # so detailed-status should be set to Deleted.
- if _has_delete_error(resp, entity_label, deleteFlag, delete_attempts_left):
- delete_attempts_left -= 1
- else:
- # Operation has finished, either with success or error
- if deleteFlag:
- delete_attempts_left -= 1
- if not wait_for_404 and delete_attempts_left < MAX_DELETE_ATTEMPTS:
- time_to_return = True
- else:
- time_to_return = True
- new_detailed_status = _get_detailed_status(resp, entity_label, detailed_status_deleted)
- # print('DETAILED-STATUS: {}'.format(new_detailed_status))
- # print('DELETE-ATTEMPTS-LEFT: {}'.format(delete_attempts_left))
- if not new_detailed_status:
- new_detailed_status = 'In progress'
- # TODO: Change LCM to provide detailed-status more up to date
- # At the moment of this writing, 'detailed-status' may return different strings
- # from different resources:
- # /nslcm/v1/ns_lcm_op_occs/<id> ---> ''
- # /nslcm/v1/ns_instances_content/<id> ---> 'deleting charms'
- detailed_status = _show_detailed_status(detailed_status, new_detailed_status)
- if time_to_return:
+ retries = 0
+ except NotFound:
+ if deleteFlag:
+ _show_detailed_status(detailed_status, 'Deleted')
return
- time_left -= POLLING_TIME_INTERVAL
+ raise
+ except ClientException:
+ if retries >= max_retries or time() < time_to_finish:
+ raise
+ retries += 1
sleep(POLLING_TIME_INTERVAL)
- if time_left <= 0:
- # There was a timeout, so raise an exception
- raise ClientException('operation timeout, waited for {} seconds'.format(timeout))
- except ClientException as exc:
- if deleteFlag and isinstance(exc, NotFound):
+ continue
+
+ resp = ''
+ if resp_unicode:
+ resp = json.loads(resp_unicode)
+
+ new_detailed_status = _get_detailed_status(resp, entity_label)
+ # print('DETAILED-STATUS: {}'.format(new_detailed_status))
+ if not new_detailed_status:
+ new_detailed_status = 'In progress'
+ detailed_status = _show_detailed_status(detailed_status, new_detailed_status)
+
+ # Get operation status
+ if _op_has_finished(resp, entity_label):
return
- message = "Operation failed for {}:\nerror:\n{}".format(
- entity_label,
- str(exc))
- raise ClientException(message)
+
+ if time() >= time_to_finish:
+ # There was a timeout, so raise an exception
+ raise ClientException('operation timeout after {} seconds'.format(timeout))
+ sleep(POLLING_TIME_INTERVAL)
self._apiVersion, self._apiResource)
# NS '--wait' option
- def _wait(self, id, deleteFlag=False):
+ def _wait(self, id, wait_time, deleteFlag=False):
self._logger.debug("")
# Endpoint to get operation status
apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/ns_lcm_op_occs')
# Wait for status for NS instance creation/update/deletion
+ if isinstance(wait_time, bool):
+ wait_time = WaitForStatus.TIMEOUT_NS_OPERATION
WaitForStatus.wait_for_status(
'NS',
str(id),
- WaitForStatus.TIMEOUT_NS_OPERATION,
+ wait_time,
apiUrlStatus,
self._http.get2_cmd,
deleteFlag=deleteFlag)
raise NotFound("ns '{}' not found".format(name))
def delete(self, name, force=False, config=None, wait=False):
+ """
+ Deletes a Network Service (NS)
+ :param name: name of network service
+ :param force: set force. Direct deletion without cleaning at VIM
+ :param config: parameters of deletion, as:
+ autoremove: Bool (default True)
+ timeout_ns_terminate: int
+ skip_terminate_primitives: Bool (default False) to not exec the terminate primitives
+ :param wait: Make synchronous. Wait until deletion is completed:
+ False to not wait (by default), True to wait a standard time, or int (time to wait)
+ :return: None. Exception if fail
+ """
self._logger.debug("")
ns = self.get(name)
querystring_list = []
if wait and resp:
resp = json.loads(resp)
# For the 'delete' operation, '_id' is used
- self._wait(resp.get('_id'), deleteFlag=True)
+ self._wait(resp.get('_id'), wait, deleteFlag=True)
else:
print('Deletion in progress')
elif http_code == 204:
resp))
if wait:
# Wait for status for NS instance creation
- self._wait(resp.get('nslcmop_id'))
+ self._wait(resp.get('nslcmop_id'), wait)
print(resp['id'])
return resp['id']
#else:
if wait:
# Wait for status for NS instance action
# For the 'action' operation, 'id' is used
- self._wait(resp.get('id'))
+ self._wait(resp.get('id'), wait)
return resp['id']
#else:
# msg = ""
op_data={}
op_data["scaleType"] = "SCALE_VNF"
op_data["scaleVnfData"] = {}
- if scale_in:
+ if scale_in and not scale_out:
op_data["scaleVnfData"]["scaleVnfType"] = "SCALE_IN"
- else:
+ elif not scale_in and scale_out:
op_data["scaleVnfData"]["scaleVnfType"] = "SCALE_OUT"
+ else:
+ raise ClientException("you must set either 'scale_in' or 'scale_out'")
op_data["scaleVnfData"]["scaleByStepData"] = {
"member-vnf-index": vnf_name,
"scaling-group-descriptor": scaling_group,