Commit bea7283d authored by Alfonso Tierno's avatar Alfonso Tierno
Browse files

fix wait option when operation fails


allow set a timeout for wait at ns,nsi,vim, wim, sdncontroller
adding wait option to vnf-scale

Change-Id: I7aa7aad8b678dcd19334a9d001e049be82476100
Signed-off-by: default avatartierno <alfonso.tiernosepulveda@telefonica.com>
parent 5d413623
Loading
Loading
Loading
Loading
+111 −128
Original line number Diff line number Diff line
@@ -20,8 +20,8 @@ OSM API handling for the '--wait' option

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
@@ -34,167 +34,150 @@ TIMEOUT_NS_OPERATION = 3600
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:
    retries = 0
    max_retries = 1
    while True:
        try:
            http_code, resp_unicode = http_cmd('{}/{}'.format(apiUrlStatus, entity_id))
            retries = 0
        except NotFound:
            if deleteFlag:
                _show_detailed_status(detailed_status, 'Deleted')
                return
            raise
        except ClientException:
            if retries >= max_retries or time() < time_to_finish:
                raise
            retries += 1
            sleep(POLLING_TIME_INTERVAL)
            continue

        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)

        new_detailed_status = _get_detailed_status(resp, entity_label)
        # 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:

        # Get operation status
        if _op_has_finished(resp, entity_label):
            return
            time_left -= POLLING_TIME_INTERVAL
            sleep(POLLING_TIME_INTERVAL)
            if time_left <= 0:

        if time() >= time_to_finish:
            # 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):
            return
        message = "Operation failed for {}:\nerror:\n{}".format(
            entity_label,
            str(exc))
        raise ClientException(message)
            raise ClientException('operation timeout after {} seconds'.format(timeout))
        sleep(POLLING_TIME_INTERVAL)
+5 −2
Original line number Diff line number Diff line
@@ -3633,13 +3633,16 @@ def ns_action(ctx,
@click.option('--scaling-group', prompt=True, help="scaling-group-descriptor name to use")
@click.option('--scale-in', default=False, is_flag=True, help="performs a scale in operation")
@click.option('--scale-out', default=False, is_flag=True, help="performs a scale out operation (by default)")
@click.option('--wait', required=False, default=False, is_flag=True,
              help='do not return the control immediately, but keep it until the operation is completed, or timeout')
@click.pass_context
def vnf_scale(ctx,
              ns_name,
              vnf_name,
              scaling_group,
              scale_in,
              scale_out):
              scale_out,
              wait):
    """
    Executes a VNF scale (adding/removing VDUs)

@@ -3652,7 +3655,7 @@ def vnf_scale(ctx,
    check_client_version(ctx.obj, ctx.command.name)
    if not scale_in and not scale_out:
        scale_out = True
    ctx.obj.ns.scale_vnf(ns_name, vnf_name, scaling_group, scale_in, scale_out)
    ctx.obj.ns.scale_vnf(ns_name, vnf_name, scaling_group, scale_in, scale_out, wait)
    # except ClientException as e:
    #     print(str(e))
    #     exit(1)
+23 −7
Original line number Diff line number Diff line
@@ -40,15 +40,17 @@ class Ns(object):
                                        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)
@@ -101,6 +103,18 @@ class Ns(object):
        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 = []
@@ -123,7 +137,7 @@ class Ns(object):
            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:
@@ -250,7 +264,7 @@ class Ns(object):
                                      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:
@@ -364,7 +378,7 @@ class Ns(object):
            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 = ""
@@ -389,10 +403,12 @@ class Ns(object):
            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,
+6 −4
Original line number Diff line number Diff line
@@ -40,16 +40,18 @@ class Nsi(object):
                                        self._apiVersion, self._apiResource)

    # NSI '--wait' option
    def _wait(self, id, deleteFlag=False):
    def _wait(self, id, wait_time, deleteFlag=False):
        self._logger.debug("")
        self._client.get_token()
        # Endpoint to get operation status
        apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/nsi_lcm_op_occs')
        # Wait for status for NSI instance creation/update/deletion
        if isinstance(wait_time, bool):
            wait_time = WaitForStatus.TIMEOUT_NSI_OPERATION
        WaitForStatus.wait_for_status(
            'NSI',
            str(id),
            WaitForStatus.TIMEOUT_NSI_OPERATION,
            wait_time,
            apiUrlStatus,
            self._http.get2_cmd,
            deleteFlag=deleteFlag)
@@ -116,7 +118,7 @@ class Nsi(object):
                resp = json.loads(resp)
                # Wait for status for NSI instance deletion
                # 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:
@@ -240,7 +242,7 @@ class Nsi(object):
                                  resp))
            if wait:
                # Wait for status for NSI instance creation
                self._wait(resp.get('nsilcmop_id'))
                self._wait(resp.get('nsilcmop_id'), wait)
            print(resp['id'])
            #else:
            #    msg = ""
+7 −5
Original line number Diff line number Diff line
@@ -39,16 +39,18 @@ class SdnController(object):
                                        self._apiVersion, self._apiResource)

    # SDNC '--wait' option
    def _wait(self, id, deleteFlag=False):
    def _wait(self, id, wait_time, deleteFlag=False):
        self._logger.debug("")
        self._client.get_token()
        # Endpoint to get operation status
        apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/sdns')
        # Wait for status for SDN instance creation/update/deletion
        if isinstance(wait_time, bool):
            wait_time = WaitForStatus.TIMEOUT_SDNC_OPERATION
        WaitForStatus.wait_for_status(
            'SDNC',
            str(id),
            WaitForStatus.TIMEOUT_SDNC_OPERATION,
            wait_time,
            apiUrlStatus,
            self._http.get2_cmd,
            deleteFlag=deleteFlag)
@@ -82,7 +84,7 @@ class SdnController(object):
                                  resp))
        if wait:
            # Wait for status for SDNC instance creation
            self._wait(resp.get('id'))
            self._wait(resp.get('id'), wait)
        print(resp['id'])
        #else:
        #    msg = ""
@@ -110,7 +112,7 @@ class SdnController(object):
            # Use the previously obtained id instead.
            wait_id = sdnc_id_for_wait
            # Wait for status for VI instance update
            self._wait(wait_id)
            self._wait(wait_id, wait)
        # else:
        #     pass
        #else:
@@ -137,7 +139,7 @@ class SdnController(object):
        if http_code == 202:
            if wait:
                # Wait for status for SDNC instance deletion
                self._wait(sdnc_id_for_wait, deleteFlag=True)
                self._wait(sdnc_id_for_wait, wait, deleteFlag=True)
            else:
                print('Deletion in progress')
        elif http_code == 204:
Loading