From 5da3f209f7595167bf762e71236076a637244c49 Mon Sep 17 00:00:00 2001 From: kuuse Date: Thu, 30 May 2019 09:42:05 +0200 Subject: [PATCH] Feature 7181: Provide real-time feedback in CLI upon request using the '--wait' option Change-Id: I238c78fa65ba5e41f2e690c4c3f0d1e8aac9de19 Signed-off-by: kuuse --- osmclient/common/wait.py | 176 +++++++++++++++++++++++ osmclient/scripts/osm.py | 223 ++++++++++++++++++++++-------- osmclient/sol005/ns.py | 48 +++++-- osmclient/sol005/nsi.py | 33 ++++- osmclient/sol005/sdncontroller.py | 44 +++++- osmclient/sol005/vim.py | 38 ++++- osmclient/sol005/wim.py | 69 +++++++-- 7 files changed, 538 insertions(+), 93 deletions(-) create mode 100644 osmclient/common/wait.py diff --git a/osmclient/common/wait.py b/osmclient/common/wait.py new file mode 100644 index 0000000..9610856 --- /dev/null +++ b/osmclient/common/wait.py @@ -0,0 +1,176 @@ +# Copyright 2019 Telefonica +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +OSM API handling for the '--wait' option +""" + +from osmclient.common.exceptions import ClientException +import json +from time import sleep +import sys + +# Declare a constant for each module, to allow customizing each timeout in the future +TIMEOUT_GENERIC_OPERATION = 600 +TIMEOUT_NSI_OPERATION = TIMEOUT_GENERIC_OPERATION +TIMEOUT_SDNC_OPERATION = TIMEOUT_GENERIC_OPERATION +TIMEOUT_VIM_OPERATION = TIMEOUT_GENERIC_OPERATION +TIMEOUT_WIM_OPERATION = TIMEOUT_GENERIC_OPERATION +TIMEOUT_NS_OPERATION = 3600 + +POLLING_TIME_INTERVAL = 1 + +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)) + return new_detailed_status + else: + return old_detailed_status + +def _get_finished_states(entity): + # Note that the member name is either: + # 'operationState' (NS and NSI) + # '_admin.'operationalState' (other) + # For NS and NSI, 'operationState' may be one of: + # PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK + # For other entities, '_admin.operationalState' may be one of: + # operationalState: ENABLED, DISABLED, ERROR, PROCESSING + if entity == 'NS' or entity == 'NSI': + return ['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED_TEMP', 'FAILED'] + else: + return ['ENABLED', 'ERROR'] + +def _get_operational_state(resp, entity): + # Note that the member name is either: + # 'operationState' (NS) + # 'operational-status' (NSI) + # '_admin.'operationalState' (other) + if entity == 'NS' or entity == 'NSI': + return resp.get('operationState') + else: + return resp.get('_admin', {}).get('operationalState') + +def _op_has_finished(resp, entity): + # _op_has_finished() 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) + 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': + # For NS and NSI, 'detailed-status' is a JSON "root" member: + return resp.get('detailed-status') + else: + # For other entities, 'detailed-status' a leaf node to '_admin': + 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__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. + + # Loop here until the operation finishes, or a timeout occurs. + time_left = timeout + detailed_status = None + detailed_status_deleted = None + time_to_return = False + delete_attempts_left = MAX_DELETE_ATTEMPTS + try: + while True: + 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 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: + if delete_attempts_left < MAX_DELETE_ATTEMPTS: + time_to_return = True + delete_attempts_left -= 1 + else: + time_to_return = True + new_detailed_status = _get_detailed_status(resp, entity_label, detailed_status_deleted) + 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/ ---> '' + # /nslcm/v1/ns_instances_content/ ---> 'deleting charms' + detailed_status = _show_detailed_status(detailed_status, new_detailed_status) + if time_to_return: + return + time_left -= POLLING_TIME_INTERVAL + 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: + message="Operation failed for {}:\nerror:\n{}".format( + entity_label, + exc.message) + raise ClientException(message) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index c500286..fa35574 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -1136,6 +1136,12 @@ def nfpkg_create(ctx, filename, overwrite): @click.option('--config_file', default=None, help='ns specific yaml configuration file') +@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 ns_create(ctx, nsd_name, @@ -1144,7 +1150,8 @@ def ns_create(ctx, admin_status, ssh_keys, config, - config_file): + config_file, + wait): """creates a new NS instance""" try: if config_file: @@ -1158,7 +1165,8 @@ def ns_create(ctx, ns_name, config=config, ssh_keys=ssh_keys, - account=vim_account) + account=vim_account, + wait=wait) except ClientException as inst: print(inst.message) exit(1) @@ -1199,7 +1207,7 @@ def nst_create2(ctx, filename, overwrite): nst_create(ctx, filename, overwrite) -def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file): +def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait): """creates a new Network Slice Instance (NSI)""" try: check_client_version(ctx.obj, ctx.command.name) @@ -1209,7 +1217,7 @@ def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_fi with open(config_file, 'r') as cf: config=cf.read() ctx.obj.nsi.create(nst_name, nsi_name, config=config, ssh_keys=ssh_keys, - account=vim_account) + account=vim_account, wait=wait) except ClientException as inst: print(inst.message) exit(1) @@ -1235,10 +1243,16 @@ def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_fi @click.option('--config_file', default=None, help='nsi specific yaml configuration file') +@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 nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file): +def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait): """creates a new Network Slice Instance (NSI)""" - nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file) + nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait=wait) @cli.command(name='netslice-instance-create', short_help='creates a new Network Slice Instance') @@ -1259,10 +1273,16 @@ def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_f @click.option('--config_file', default=None, help='nsi specific yaml configuration file') +@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 nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file): +def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait): """creates a new Network Slice Instance (NSI)""" - nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file) + nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file, wait=wait) @cli.command(name='pdu-create', short_help='adds a new Physical Deployment Unit to the catalog') @@ -1523,18 +1543,24 @@ def nfpkg_delete(ctx, name, force): @cli.command(name='ns-delete', short_help='deletes a NS instance') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@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 ns_delete(ctx, name, force): +def ns_delete(ctx, name, force, wait): """deletes a NS instance NAME: name or ID of the NS instance to be deleted """ try: if not force: - ctx.obj.ns.delete(name) + ctx.obj.ns.delete(name, wait=wait) else: check_client_version(ctx.obj, '--force') - ctx.obj.ns.delete(name, force) + ctx.obj.ns.delete(name, force, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1573,10 +1599,10 @@ def nst_delete2(ctx, name, force): nst_delete(ctx, name, force) -def nsi_delete(ctx, name, force): +def nsi_delete(ctx, name, force, wait): try: check_client_version(ctx.obj, ctx.command.name) - ctx.obj.nsi.delete(name, force) + ctx.obj.nsi.delete(name, force, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1585,25 +1611,31 @@ def nsi_delete(ctx, name, force): @cli.command(name='nsi-delete', short_help='deletes a Network Slice Instance (NSI)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@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 nsi_delete1(ctx, name, force): +def nsi_delete1(ctx, name, force, wait): """deletes a Network Slice Instance (NSI) NAME: name or ID of the Network Slice instance to be deleted """ - nsi_delete(ctx, name, force) + nsi_delete(ctx, name, force, wait=wait) @cli.command(name='netslice-instance-delete', short_help='deletes a Network Slice Instance (NSI)') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') @click.pass_context -def nsi_delete2(ctx, name, force): +def nsi_delete2(ctx, name, force, wait): """deletes a Network Slice Instance (NSI) NAME: name or ID of the Network Slice instance to be deleted """ - nsi_delete(ctx, name, force) + nsi_delete(ctx, name, force, wait=wait) @cli.command(name='pdu-delete', short_help='deletes a Physical Deployment Unit (PDU)') @@ -1656,6 +1688,12 @@ def pdu_delete(ctx, name, force): help='human readable description') @click.option('--sdn_controller', default=None, help='Name or id of the SDN controller associated to this VIM account') @click.option('--sdn_port_mapping', default=None, help="File describing the port mapping between compute nodes' ports and switch ports") +@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 vim_create(ctx, name, @@ -1667,7 +1705,8 @@ def vim_create(ctx, account_type, description, sdn_controller, - sdn_port_mapping): + sdn_port_mapping, + wait): """creates a new VIM account""" try: if sdn_controller: @@ -1683,9 +1722,9 @@ def vim_create(ctx, vim['description'] = description vim['config'] = config if sdn_controller or sdn_port_mapping: - ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping) + ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping, wait=wait) else: - ctx.obj.vim.create(name, vim) + ctx.obj.vim.create(name, vim, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1703,6 +1742,12 @@ def vim_create(ctx, @click.option('--description', help='human readable description') @click.option('--sdn_controller', default=None, help='Name or id of the SDN controller associated to this VIM account') @click.option('--sdn_port_mapping', default=None, help="File describing the port mapping between compute nodes' ports and switch ports") +@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 vim_update(ctx, name, @@ -1715,7 +1760,8 @@ def vim_update(ctx, account_type, description, sdn_controller, - sdn_port_mapping): + sdn_port_mapping, + wait): """updates a VIM account NAME: name or ID of the VIM account @@ -1731,7 +1777,7 @@ def vim_update(ctx, if account_type: vim['vim_type'] = account_type if description: vim['description'] = description if config: vim['config'] = config - ctx.obj.vim.update(name, vim, sdn_controller, sdn_port_mapping) + ctx.obj.vim.update(name, vim, sdn_controller, sdn_port_mapping, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1740,18 +1786,24 @@ def vim_update(ctx, @cli.command(name='vim-delete') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@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 vim_delete(ctx, name, force): +def vim_delete(ctx, name, force, wait): """deletes a VIM account NAME: name or ID of the VIM account to be deleted """ try: if not force: - ctx.obj.vim.delete(name) + ctx.obj.vim.delete(name, wait=wait) else: check_client_version(ctx.obj, '--force') - ctx.obj.vim.delete(name, force) + ctx.obj.vim.delete(name, force, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1831,6 +1883,12 @@ def vim_show(ctx, name): default='no description', help='human readable description') @click.option('--wim_port_mapping', default=None, help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge (WAN service endpoint id and info)") +@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 wim_create(ctx, name, @@ -1841,7 +1899,8 @@ def wim_create(ctx, config, wim_type, description, - wim_port_mapping): + wim_port_mapping, + wait): """creates a new WIM account""" try: check_client_version(ctx.obj, ctx.command.name) @@ -1857,7 +1916,7 @@ def wim_create(ctx, wim['wim_type'] = wim_type if description: wim['description'] = description if config: wim['config'] = config - ctx.obj.wim.create(name, wim, wim_port_mapping) + ctx.obj.wim.create(name, wim, wim_port_mapping, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1873,6 +1932,12 @@ def wim_create(ctx, @click.option('--wim_type', help='WIM type') @click.option('--description', help='human readable description') @click.option('--wim_port_mapping', default=None, help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge (WAN service endpoint id and info)") +@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 wim_update(ctx, name, @@ -1883,7 +1948,8 @@ def wim_update(ctx, config, wim_type, description, - wim_port_mapping): + wim_port_mapping, + wait): """updates a WIM account NAME: name or ID of the WIM account @@ -1899,7 +1965,7 @@ def wim_update(ctx, if wim_type: wim['wim_type'] = wim_type if description: wim['description'] = description if config: wim['config'] = config - ctx.obj.wim.update(name, wim, wim_port_mapping) + ctx.obj.wim.update(name, wim, wim_port_mapping, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1908,15 +1974,21 @@ def wim_update(ctx, @cli.command(name='wim-delete') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@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 wim_delete(ctx, name, force): +def wim_delete(ctx, name, force, wait): """deletes a WIM account NAME: name or ID of the WIM account to be deleted """ try: check_client_version(ctx.obj, ctx.command.name) - ctx.obj.wim.delete(name, force) + ctx.obj.wim.delete(name, force, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -1977,7 +2049,7 @@ def wim_show(ctx, name): prompt=True, help='SDN controller type') @click.option('--sdn_controller_version', - help='SDN controller username') + help='SDN controller version') @click.option('--ip_address', prompt=True, help='SDN controller IP address') @@ -1996,16 +2068,23 @@ def wim_show(ctx, name): #@click.option('--description', # default='no description', # help='human readable description') +@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 sdnc_create(ctx, - name, - type, - sdn_controller_version, - ip_address, - port, - switch_dpid, - user, - password): + name, + type, + sdn_controller_version, + ip_address, + port, + switch_dpid, + user, + password, + wait): """creates a new SDN controller""" sdncontroller = {} sdncontroller['name'] = name @@ -2022,7 +2101,7 @@ def sdnc_create(ctx, # sdncontroller['description'] = description try: check_client_version(ctx.obj, ctx.command.name) - ctx.obj.sdnc.create(name, sdncontroller) + ctx.obj.sdnc.create(name, sdncontroller, wait=wait) except ClientException as inst: print((inst.message)) @@ -2038,17 +2117,24 @@ def sdnc_create(ctx, @click.option('--user', help='SDN controller username') @click.option('--password', help='SDN controller password') #@click.option('--description', default=None, help='human readable description') +@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 sdnc_update(ctx, - name, - newname, - type, - sdn_controller_version, - ip_address, - port, - switch_dpid, - user, - password): + name, + newname, + type, + sdn_controller_version, + ip_address, + port, + switch_dpid, + user, + password, + wait): """updates an SDN controller NAME: name or ID of the SDN controller @@ -2077,7 +2163,7 @@ def sdnc_update(ctx, sdncontroller['password'] = user try: check_client_version(ctx.obj, ctx.command.name) - ctx.obj.sdnc.update(name, sdncontroller) + ctx.obj.sdnc.update(name, sdncontroller, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -2086,15 +2172,21 @@ def sdnc_update(ctx, @cli.command(name='sdnc-delete') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@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 sdnc_delete(ctx, name, force): +def sdnc_delete(ctx, name, force, wait): """deletes an SDN controller NAME: name or ID of the SDN controller to be deleted """ try: check_client_version(ctx.obj, ctx.command.name) - ctx.obj.sdnc.delete(name, force) + ctx.obj.sdnc.delete(name, force, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -2422,10 +2514,10 @@ def ns_alarm_create(ctx, name, ns, vnf, vdu, metric, severity, #@click.argument('name') #@click.pass_context #def ns_alarm_delete(ctx, name): -# '''deletes an alarm +# """deletes an alarm # # NAME: name of the alarm to be deleted -# ''' +# """ # try: # check_client_version(ctx.obj, ctx.command.name) # ctx.obj.ns.delete_alarm(name) @@ -2544,15 +2636,21 @@ def show_ns_scaling(ctx, ns_name): @click.argument('ns_name') @click.option('--ns_scale_group', prompt=True) @click.option('--index', prompt=True) +@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 ns_scale(ctx, ns_name, ns_scale_group, index): +def ns_scale(ctx, ns_name, ns_scale_group, index, wait): """scales NS NS_NAME: name of the NS instance to be scaled """ try: check_client_version(ctx.obj, ctx.command.name, 'v1') - ctx.obj.ns.scale(ns_name, ns_scale_group, index) + ctx.obj.ns.scale(ns_name, ns_scale_group, index, wait=wait) except ClientException as inst: print((inst.message)) exit(1) @@ -2647,12 +2745,19 @@ def vcs_list(ctx): @click.option('--vnf_name', default=None) @click.option('--action_name', prompt=True) @click.option('--params', prompt=True) +@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 ns_action(ctx, ns_name, vnf_name, action_name, - params): + params, + wait): """executes an action/primitive over a NS instance NS_NAME: name or ID of the NS instance @@ -2664,7 +2769,7 @@ def ns_action(ctx, op_data['vnf_member_index'] = vnf_name op_data['primitive'] = action_name op_data['primitive_params'] = yaml.load(params) - ctx.obj.ns.exec_op(ns_name, op_name='action', op_data=op_data) + ctx.obj.ns.exec_op(ns_name, op_name='action', op_data=op_data, wait=wait) except ClientException as inst: print((inst.message)) diff --git a/osmclient/sol005/ns.py b/osmclient/sol005/ns.py index 34cb683..0e178c2 100644 --- a/osmclient/sol005/ns.py +++ b/osmclient/sol005/ns.py @@ -19,6 +19,7 @@ OSM ns API handling """ from osmclient.common import utils +from osmclient.common import wait as WaitForStatus from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml @@ -36,6 +37,19 @@ class Ns(object): self._apiBase = '{}{}{}'.format(self._apiName, self._apiVersion, self._apiResource) + # NS '--wait' option + def _wait(self, id, deleteFlag=False): + # Endpoint to get operation status + apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/ns_lcm_op_occs') + # Wait for status for NS instance creation/update/deletion + WaitForStatus.wait_for_status( + 'NS', + str(id), + WaitForStatus.TIMEOUT_NS_OPERATION, + apiUrlStatus, + self._http.get2_cmd, + deleteFlag=deleteFlag) + def list(self, filter=None): """Returns a list of NS """ @@ -74,17 +88,22 @@ class Ns(object): return resp raise NotFound("ns {} not found".format(name)) - def delete(self, name, force=False): + def delete(self, name, force=False, wait=False): ns = self.get(name) querystring = '' if force: querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, - ns['_id'], querystring)) - #print 'HTTP CODE: {}'.format(http_code) - #print 'RESP: {}'.format(resp) + ns['_id'], querystring)) + # print 'HTTP CODE: {}'.format(http_code) + # print 'RESP: {}'.format(resp) if http_code == 202: - print('Deletion in progress') + if wait and resp: + resp = json.loads(resp) + # For the 'delete' operation, '_id' is used + self._wait(resp.get('_id'), deleteFlag=True) + else: + print('Deletion in progress') elif http_code == 204: print('Deleted') else: @@ -98,7 +117,7 @@ class Ns(object): def create(self, nsd_name, nsr_name, account, config=None, ssh_keys=None, description='default description', - admin_status='ENABLED'): + admin_status='ENABLED', wait=False): nsd = self._client.nsd.get(nsd_name) @@ -196,14 +215,17 @@ class Ns(object): self._http.set_http_header(http_header) http_code, resp = self._http.post_cmd(endpoint=self._apiBase, postfields_dict=ns) - #print 'HTTP CODE: {}'.format(http_code) - #print 'RESP: {}'.format(resp) + # print 'HTTP CODE: {}'.format(http_code) + # print 'RESP: {}'.format(resp) if http_code in (200, 201, 202, 204): if resp: resp = json.loads(resp) if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {} '.format( resp)) + if wait: + # Wait for status for NS instance creation + self._wait(resp.get('nslcmop_id')) return resp['id'] else: msg = "" @@ -288,7 +310,7 @@ class Ns(object): exc.message) raise ClientException(message) - def exec_op(self, name, op_name, op_data=None): + def exec_op(self, name, op_name, op_data=None, wait=False): """Executes an operation on a NS """ ns = self.get(name) @@ -308,6 +330,10 @@ class Ns(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( resp)) + if wait: + # Wait for status for NS instance action + # For the 'action' operation, 'id' is used + self._wait(resp.get('id')) print(resp['id']) else: msg = "" @@ -323,7 +349,7 @@ class Ns(object): exc.message) raise ClientException(message) - def scale_vnf(self, ns_name, vnf_name, scaling_group, scale_in, scale_out): + def scale_vnf(self, ns_name, vnf_name, scaling_group, scale_in, scale_out, wait=False): """Scales a VNF by adding/removing VDUs """ try: @@ -338,7 +364,7 @@ class Ns(object): "member-vnf-index": vnf_name, "scaling-group-descriptor": scaling_group, } - self.exec_op(ns_name, op_name='scale', op_data=op_data) + self.exec_op(ns_name, op_name='scale', op_data=op_data, wait=wait) except ClientException as exc: message="failed to scale vnf {} of ns {}:\nerror:\n{}".format( vnf_name, ns_name, exc.message) diff --git a/osmclient/sol005/nsi.py b/osmclient/sol005/nsi.py index cb87b85..392d1ab 100644 --- a/osmclient/sol005/nsi.py +++ b/osmclient/sol005/nsi.py @@ -19,6 +19,7 @@ OSM NSI (Network Slice Instance) API handling """ from osmclient.common import utils +from osmclient.common import wait as WaitForStatus from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml @@ -36,6 +37,19 @@ class Nsi(object): self._apiBase = '{}{}{}'.format(self._apiName, self._apiVersion, self._apiResource) + # NSI '--wait' option + def _wait(self, id, deleteFlag=False): + # Endpoint to get operation status + apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/nsi_lcm_op_occs') + # Wait for status for NSI instance creation/update/deletion + WaitForStatus.wait_for_status( + 'NSI', + str(id), + WaitForStatus.TIMEOUT_NSI_OPERATION, + apiUrlStatus, + self._http.get2_cmd, + deleteFlag=deleteFlag) + def list(self, filter=None): """Returns a list of NSI """ @@ -74,17 +88,23 @@ class Nsi(object): return resp raise NotFound("nsi {} not found".format(name)) - def delete(self, name, force=False): + def delete(self, name, force=False, wait=False): nsi = self.get(name) querystring = '' if force: querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, nsi['_id'], querystring)) - #print 'HTTP CODE: {}'.format(http_code) - #print 'RESP: {}'.format(resp) + # print 'HTTP CODE: {}'.format(http_code) + # print 'RESP: {}'.format(resp) if http_code == 202: - print('Deletion in progress') + if wait and resp: + 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) + else: + print('Deletion in progress') elif http_code == 204: print('Deleted') else: @@ -98,7 +118,7 @@ class Nsi(object): def create(self, nst_name, nsi_name, account, config=None, ssh_keys=None, description='default description', - admin_status='ENABLED'): + admin_status='ENABLED', wait=False): nst = self._client.nst.get(nst_name) @@ -199,6 +219,9 @@ class Nsi(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {} '.format( resp)) + if wait: + # Wait for status for NSI instance creation + self._wait(resp.get('nsilcmop_id')) print(resp['id']) else: msg = "" diff --git a/osmclient/sol005/sdncontroller.py b/osmclient/sol005/sdncontroller.py index 391089f..6a6c1c1 100644 --- a/osmclient/sol005/sdncontroller.py +++ b/osmclient/sol005/sdncontroller.py @@ -19,6 +19,7 @@ OSM SDN controller API handling """ from osmclient.common import utils +from osmclient.common import wait as WaitForStatus from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import json @@ -34,7 +35,30 @@ class SdnController(object): self._apiBase = '{}{}{}'.format(self._apiName, self._apiVersion, self._apiResource) - def create(self, name, sdn_controller): + # SDNC '--wait' option + def _wait(self, id, deleteFlag=False): + # Endpoint to get operation status + apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/sdns') + # Wait for status for SDN instance creation/update/deletion + WaitForStatus.wait_for_status( + 'SDNC', + str(id), + WaitForStatus.TIMEOUT_SDNC_OPERATION, + apiUrlStatus, + self._http.get2_cmd, + deleteFlag=deleteFlag) + + def _get_id_for_wait(self, name): + # Returns id of name, or the id itself if given as argument + for sdnc in self.list(): + if name == sdnc['name']: + return sdnc['_id'] + for wim in self.list(): + if name == sdnc['_id']: + return sdnc['_id'] + return '' + + def create(self, name, sdn_controller, wait=False): http_code, resp = self._http.post_cmd(endpoint=self._apiBase, postfields_dict=sdn_controller) #print 'HTTP CODE: {}'.format(http_code) @@ -45,6 +69,9 @@ class SdnController(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( resp)) + if wait: + # Wait for status for SDNC instance creation + self._wait(resp.get('id')) print(resp['id']) else: msg = "" @@ -55,8 +82,9 @@ class SdnController(object): msg = resp raise ClientException("failed to create SDN controller {} - {}".format(name, msg)) - def update(self, name, sdn_controller): + def update(self, name, sdn_controller, wait=False): sdnc = self.get(name) + sdnc_id_for_wait = self._get_id_for_wait(name) http_code, resp = self._http.put_cmd(endpoint='{}/{}'.format(self._apiBase,sdnc['_id']), postfields_dict=sdn_controller) #print 'HTTP CODE: {}'.format(http_code) @@ -67,6 +95,9 @@ class SdnController(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( resp)) + if wait: + # Wait for status for SDNC instance update + self._wait(sdnc_id_for_wait) print(resp['id']) else: msg = "" @@ -77,8 +108,9 @@ class SdnController(object): msg = resp raise ClientException("failed to update SDN controller {} - {}".format(name, msg)) - def delete(self, name, force=False): + def delete(self, name, force=False, wait=False): sdn_controller = self.get(name) + sdnc_id_for_wait = self._get_id_for_wait(name) querystring = '' if force: querystring = '?FORCE=True' @@ -87,7 +119,11 @@ class SdnController(object): #print 'HTTP CODE: {}'.format(http_code) #print 'RESP: {}'.format(resp) if http_code == 202: - print('Deletion in progress') + if wait: + # Wait for status for SDNC instance deletion + self._wait(sdnc_id_for_wait, deleteFlag=True) + else: + print('Deletion in progress') elif http_code == 204: print('Deleted') elif resp and 'result' in resp: diff --git a/osmclient/sol005/vim.py b/osmclient/sol005/vim.py index 293362a..bd95d66 100644 --- a/osmclient/sol005/vim.py +++ b/osmclient/sol005/vim.py @@ -19,6 +19,7 @@ OSM vim API handling """ from osmclient.common import utils +from osmclient.common import wait as WaitForStatus from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml @@ -34,7 +35,20 @@ class Vim(object): self._apiResource = '/vim_accounts' self._apiBase = '{}{}{}'.format(self._apiName, self._apiVersion, self._apiResource) - def create(self, name, vim_access, sdn_controller=None, sdn_port_mapping=None): + # VIM '--wait' option + def _wait(self, id, deleteFlag=False): + # Endpoint to get operation status + apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/vim_accounts') + # Wait for status for VIM instance creation/deletion + WaitForStatus.wait_for_status( + 'VIM', + str(id), + WaitForStatus.TIMEOUT_VIM_OPERATION, + apiUrlStatus, + self._http.get2_cmd, + deleteFlag=deleteFlag) + + def create(self, name, vim_access, sdn_controller=None, sdn_port_mapping=None, wait=False): if 'vim-type' not in vim_access: #'openstack' not in vim_access['vim-type']): raise Exception("vim type not provided") @@ -66,6 +80,9 @@ class Vim(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( resp)) + if wait: + # Wait for status for VIM instance creation + self._wait(resp.get('id')) print(resp['id']) else: msg = "" @@ -76,7 +93,7 @@ class Vim(object): msg = resp raise ClientException("failed to create vim {} - {}".format(name, msg)) - def update(self, vim_name, vim_account, sdn_controller, sdn_port_mapping): + def update(self, vim_name, vim_account, sdn_controller, sdn_port_mapping, wait=False): vim = self.get(vim_name) vim_config = {} @@ -105,6 +122,9 @@ class Vim(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( resp)) + if wait: + # Wait for status for VIM instance update + self._wait(resp.get('id')) print(resp['id']) else: msg = "" @@ -132,7 +152,7 @@ class Vim(object): return vim['uuid'] raise NotFound("vim {} not found".format(name)) - def delete(self, vim_name, force=False): + def delete(self, vim_name, force=False, wait=False): vim_id = vim_name if not utils.validate_uuid4(vim_name): vim_id = self.get_id(vim_name) @@ -144,7 +164,17 @@ class Vim(object): #print 'HTTP CODE: {}'.format(http_code) #print 'RESP: {}'.format(resp) if http_code == 202: - print('Deletion in progress') + if wait: + # When deleting an account, 'resp' may be None. + # In such a case, the 'id' from 'resp' cannot be used, so use 'vim_id' instead + wait_id = vim_id + if resp: + resp = json.loads(resp) + wait_id = resp.get('id') + # Wait for status for VIM account deletion + self._wait(wait_id, deleteFlag=True) + else: + print('Deletion in progress') elif http_code == 204: print('Deleted') else: diff --git a/osmclient/sol005/wim.py b/osmclient/sol005/wim.py index d2721cd..defc536 100644 --- a/osmclient/sol005/wim.py +++ b/osmclient/sol005/wim.py @@ -19,6 +19,7 @@ OSM wim API handling """ from osmclient.common import utils +from osmclient.common import wait as WaitForStatus from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import yaml @@ -34,7 +35,30 @@ class Wim(object): self._apiResource = '/wim_accounts' self._apiBase = '{}{}{}'.format(self._apiName, self._apiVersion, self._apiResource) - def create(self, name, wim_input, wim_port_mapping=None): + # WIM '--wait' option + def _wait(self, id, deleteFlag=False): + # Endpoint to get operation status + apiUrlStatus = '{}{}{}'.format(self._apiName, self._apiVersion, '/wim_accounts') + # Wait for status for WIM instance creation/deletion + WaitForStatus.wait_for_status( + 'WIM', + str(id), + WaitForStatus.TIMEOUT_WIM_OPERATION, + apiUrlStatus, + self._http.get2_cmd, + deleteFlag=deleteFlag) + + def _get_id_for_wait(self, name): + # Returns id of name, or the id itself if given as argument + for wim in self.list(): + if name == wim['name']: + return wim['uuid'] + for wim in self.list(): + if name == wim['uuid']: + return wim['uuid'] + return '' + + def create(self, name, wim_input, wim_port_mapping=None, wait=False): if 'wim_type' not in wim_input: raise Exception("wim type not provided") @@ -61,6 +85,9 @@ class Wim(object): if not resp or 'id' not in resp: raise ClientException('unexpected response from server - {}'.format( resp)) + if wait: + # Wait for status for WIM instance creation + self._wait(resp.get('id')) print(resp['id']) else: msg = "" @@ -71,9 +98,9 @@ class Wim(object): msg = resp raise ClientException("failed to create wim {} - {}".format(name, msg)) - def update(self, wim_name, wim_account, wim_port_mapping=None): + def update(self, wim_name, wim_account, wim_port_mapping=None, wait=False): wim = self.get(wim_name) - + wim_id_for_wait = self._get_id_for_wait(wim_name) wim_config = {} if 'config' in wim_account: if wim_account.get('config')=="" and (wim_port_mapping): @@ -92,7 +119,18 @@ class Wim(object): #print 'HTTP CODE: {}'.format(http_code) #print 'RESP: {}'.format(resp) if http_code in (200, 201, 202, 204): - pass + if wait: + # 'resp' may be None. + # In that case, 'resp['id']' cannot be used. + # In that case, 'resp['id']' cannot be used, so use the previously obtained id instead + wait_id = wim_id_for_wait + if resp: + resp = json.loads(resp) + wait_id = resp.get('id') + # Wait for status for WIM instance update + self._wait(wait_id) + else: + pass else: msg = "" if resp: @@ -112,15 +150,16 @@ class Wim(object): return wim_account def get_id(self, name): - """Returns a VIM id from a VIM name + """Returns a WIM id from a WIM name """ for wim in self.list(): if name == wim['name']: return wim['uuid'] raise NotFound("wim {} not found".format(name)) - def delete(self, wim_name, force=False): + def delete(self, wim_name, force=False, wait=False): wim_id = wim_name + wim_id_for_wait = self._get_id_for_wait(wim_name) if not utils.validate_uuid4(wim_name): wim_id = self.get_id(wim_name) querystring = '' @@ -128,10 +167,21 @@ class Wim(object): querystring = '?FORCE=True' http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, wim_id, querystring)) - #print 'HTTP CODE: {}'.format(http_code) - #print 'RESP: {}'.format(resp) + # print 'HTTP CODE: {}'.format(http_code) + # print 'RESP: {}'.format(resp) + # print 'WIM_ID: {}'.format(wim_id) if http_code == 202: - print('Deletion in progress') + if wait: + # 'resp' may be None. + # In that case, 'resp['id']' cannot be used, so use the previously obtained id instead + wait_id = wim_id_for_wait + if resp: + resp = json.loads(resp) + wait_id = resp.get('id') + # Wait for status for WIM account deletion + self._wait(wait_id, deleteFlag=True) + else: + print('Deletion in progress') elif http_code == 204: print('Deleted') else: @@ -171,4 +221,3 @@ class Wim(object): else: return resp raise NotFound("wim {} not found".format(name)) - -- 2.17.1