1 # Copyright 2019 Telefonica
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
18 OSM API handling for the '--wait' option
21 from osmclient
.common
.exceptions
import ClientException
23 from time
import sleep
26 # Declare a constant for each module, to allow customizing each timeout in the future
27 TIMEOUT_GENERIC_OPERATION
= 600
28 TIMEOUT_NSI_OPERATION
= TIMEOUT_GENERIC_OPERATION
29 TIMEOUT_SDNC_OPERATION
= TIMEOUT_GENERIC_OPERATION
30 TIMEOUT_VIM_OPERATION
= TIMEOUT_GENERIC_OPERATION
31 TIMEOUT_WIM_OPERATION
= TIMEOUT_GENERIC_OPERATION
32 TIMEOUT_NS_OPERATION
= 3600
33 POLLING_TIME_INTERVAL
= 5
34 MAX_DELETE_ATTEMPTS
= 3
36 def _show_detailed_status(old_detailed_status
, new_detailed_status
):
37 if new_detailed_status
is not None and new_detailed_status
!= old_detailed_status
:
38 sys
.stderr
.write("detailed-status: {}\n".format(new_detailed_status
))
39 return new_detailed_status
41 return old_detailed_status
43 def _get_finished_states(entity
):
44 # Note that the member name is either:
45 # 'operationState' (NS, NSI)
46 # '_admin.'operationalState' (VIM, WIM, SDN)
47 # For NS and NSI, 'operationState' may be one of:
48 # PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK
49 # For VIM, WIM, SDN: '_admin.operationalState' may be one of:
50 # operationalState: ENABLED, DISABLED, ERROR, PROCESSING
51 if entity
== 'NS' or entity
== 'NSI':
52 return ['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED_TEMP', 'FAILED']
54 return ['ENABLED', 'ERROR']
56 def _get_operational_state(resp
, entity
):
57 # Note that the member name is either:
58 # 'operationState' (NS)
59 # 'operational-status' (NSI)
60 # '_admin.'operationalState' (other)
61 if entity
== 'NS' or entity
== 'NSI':
62 return resp
.get('operationState')
64 return resp
.get('_admin', {}).get('operationalState')
66 def _op_has_finished(resp
, entity
):
67 # This function returns:
68 # 0 on success (operation has finished)
69 # 1 on pending (operation has not finished)
70 # -1 on error (bad response)
72 finished_states
= _get_finished_states(entity
)
74 operationalState
= _get_operational_state(resp
, entity
)
76 if operationalState
in finished_states
:
81 def _get_detailed_status(resp
, entity
, detailed_status_deleted
):
82 if detailed_status_deleted
:
83 return detailed_status_deleted
84 if entity
== 'NS' or entity
== 'NSI':
85 # For NS and NSI, 'detailed-status' is a JSON "root" member:
86 return resp
.get('detailed-status')
88 # For VIM, WIM, SDN, 'detailed-status' is either:
89 # - a leaf node to '_admin' (operations NOT supported)
90 # - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI)
91 # https://osm.etsi.org/gerrit/#/c/7767 : LCM support for operations
92 # https://osm.etsi.org/gerrit/#/c/7734 : NBI support for current_operation
93 ops
= resp
.get('_admin', {}).get('operations')
94 op_index
= resp
.get('_admin', {}).get('current_operation')
96 # Operations are supported, verify operation index
97 if isinstance(op_index
, (int)) or op_index
.isdigit():
98 op_index
= int(op_index
)
99 if op_index
> 0 and op_index
< len(ops
) and ops
[op_index
] and ops
[op_index
]["detailed-status"]:
100 return ops
[op_index
]["detailed-status"]
101 # operation index is either non-numeric or out-of-range
102 return 'Unexpected error when getting detailed-status!'
104 # Operations are NOT supported
105 return resp
.get('_admin', {}).get('detailed-status')
107 def _has_delete_error(resp
, entity
, deleteFlag
, delete_attempts_left
):
108 if deleteFlag
and delete_attempts_left
:
109 state
= _get_operational_state(resp
, entity
)
110 if state
and state
== 'ERROR':
114 def wait_for_status(entity_label
, entity_id
, timeout
, apiUrlStatus
, http_cmd
, deleteFlag
=False):
116 # entity_label: String describing the entities using '--wait':
117 # 'NS', 'NSI', 'SDNC', 'VIM', 'WIM'
118 # entity_id: The ID for an existing entity, the operation ID for an entity to create.
119 # timeout: See section at top of this file for each value of TIMEOUT_<ENTITY>_OPERATION
120 # apiUrlStatus: The endpoint to get the Response including 'detailed-status'
121 # http_cmd: callback to HTTP command.
122 # Passing this callback as an argument avoids importing the 'http' module here.
124 # Loop here until the operation finishes, or a timeout occurs.
126 detailed_status
= None
127 detailed_status_deleted
= None
128 time_to_return
= False
129 delete_attempts_left
= MAX_DELETE_ATTEMPTS
133 http_code
, resp_unicode
= http_cmd('{}/{}'.format(apiUrlStatus
, entity_id
))
136 resp
= json
.loads(resp_unicode
)
137 # print('HTTP CODE: {}'.format(http_code))
138 # print('RESP: {}'.format(resp))
139 # print('URL: {}/{}'.format(apiUrlStatus, entity_id))
140 if deleteFlag
and http_code
== 404:
141 # In case of deletion, '404 Not Found' means successfully deleted
142 # Display 'detailed-status: Deleted' and return
143 time_to_return
= True
144 detailed_status_deleted
= 'Deleted'
145 elif deleteFlag
and http_code
in (200, 201, 202, 204):
146 # In case of deletion and HTTP Status = 20* OK, deletion may be PROCESSING or COMPLETED
147 # If this is the case, we should keep on polling until 404 (deleted) is returned.
149 elif http_code
not in (200, 201, 202, 204):
150 raise ClientException(str(resp
))
151 if not time_to_return
:
152 # Get operation status
153 op_status
= _op_has_finished(resp
, entity_label
)
156 raise ClientException('unexpected response from server - {} '.format(
159 # If there was an error upon deletion, try again to delete the same instance
160 # If the error is the same, there is probably nothing we can do but exit with error.
161 # If the error is different (i.e. 404), the instance was probably already corrupt, that is,
162 # operation(al)State was probably ERROR before deletion.
163 # In such a case, even if the previous state was ERROR, the deletion was successful,
164 # so detailed-status should be set to Deleted.
165 if _has_delete_error(resp
, entity_label
, deleteFlag
, delete_attempts_left
):
166 delete_attempts_left
-= 1
168 # Operation has finished, either with success or error
170 delete_attempts_left
-= 1
171 if not wait_for_404
and delete_attempts_left
< MAX_DELETE_ATTEMPTS
:
172 time_to_return
= True
174 time_to_return
= True
175 new_detailed_status
= _get_detailed_status(resp
, entity_label
, detailed_status_deleted
)
176 # print('DETAILED-STATUS: {}'.format(new_detailed_status))
177 # print('DELETE-ATTEMPTS-LEFT: {}'.format(delete_attempts_left))
178 if not new_detailed_status
:
179 new_detailed_status
= 'In progress'
180 # TODO: Change LCM to provide detailed-status more up to date
181 # At the moment of this writing, 'detailed-status' may return different strings
182 # from different resources:
183 # /nslcm/v1/ns_lcm_op_occs/<id> ---> ''
184 # /nslcm/v1/ns_instances_content/<id> ---> 'deleting charms'
185 detailed_status
= _show_detailed_status(detailed_status
, new_detailed_status
)
188 time_left
-= POLLING_TIME_INTERVAL
189 sleep(POLLING_TIME_INTERVAL
)
191 # There was a timeout, so raise an exception
192 raise ClientException('operation timeout, waited for {} seconds'.format(timeout
))
193 except ClientException
as exc
:
194 message
="Operation failed for {}:\nerror:\n{}".format(
197 raise ClientException(message
)