e3152af145cbe8cdbd4636d452ae115c46c45b88
[osm/osmclient.git] / osmclient / common / wait.py
1 # Copyright 2019 Telefonica
2 #
3 # All Rights Reserved.
4 #
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
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
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
15 # under the License.
16
17 """
18 OSM API handling for the '--wait' option
19 """
20
21 from osmclient.common.exceptions import ClientException
22 import json
23 from time import sleep
24 import sys
25
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_K8S_OPERATION = TIMEOUT_GENERIC_OPERATION
32 TIMEOUT_WIM_OPERATION = TIMEOUT_GENERIC_OPERATION
33 TIMEOUT_NS_OPERATION = 3600
34 POLLING_TIME_INTERVAL = 5
35 MAX_DELETE_ATTEMPTS = 3
36
37 def _show_detailed_status(old_detailed_status, new_detailed_status):
38 if new_detailed_status is not None and new_detailed_status != old_detailed_status:
39 sys.stderr.write("detailed-status: {}\n".format(new_detailed_status))
40 return new_detailed_status
41 else:
42 return old_detailed_status
43
44 def _get_finished_states(entity):
45 # Note that the member name is either:
46 # 'operationState' (NS, NSI)
47 # '_admin.'operationalState' (VIM, WIM, SDN)
48 # For NS and NSI, 'operationState' may be one of:
49 # PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK
50 # For VIM, WIM, SDN: '_admin.operationalState' may be one of:
51 # operationalState: ENABLED, DISABLED, ERROR, PROCESSING
52 if entity == 'NS' or entity == 'NSI':
53 return ['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED_TEMP', 'FAILED']
54 else:
55 return ['ENABLED', 'ERROR']
56
57 def _get_operational_state(resp, entity):
58 # Note that the member name is either:
59 # 'operationState' (NS)
60 # 'operational-status' (NSI)
61 # '_admin.'operationalState' (other)
62 if entity == 'NS' or entity == 'NSI':
63 return resp.get('operationState')
64 else:
65 return resp.get('_admin', {}).get('operationalState')
66
67 def _op_has_finished(resp, entity):
68 # This function returns:
69 # 0 on success (operation has finished)
70 # 1 on pending (operation has not finished)
71 # -1 on error (bad response)
72 #
73 finished_states = _get_finished_states(entity)
74 if resp:
75 operationalState = _get_operational_state(resp, entity)
76 if operationalState:
77 if operationalState in finished_states:
78 return 0
79 return 1
80 return -1
81
82 def _get_detailed_status(resp, entity, detailed_status_deleted):
83 if detailed_status_deleted:
84 return detailed_status_deleted
85 if entity == 'NS' or entity == 'NSI':
86 # For NS and NSI, 'detailed-status' is a JSON "root" member:
87 return resp.get('detailed-status')
88 else:
89 # For VIM, WIM, SDN, 'detailed-status' is either:
90 # - a leaf node to '_admin' (operations NOT supported)
91 # - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI)
92 # https://osm.etsi.org/gerrit/#/c/7767 : LCM support for operations
93 # https://osm.etsi.org/gerrit/#/c/7734 : NBI support for current_operation
94 ops = resp.get('_admin', {}).get('operations')
95 op_index = resp.get('_admin', {}).get('current_operation')
96 if ops and op_index:
97 # Operations are supported, verify operation index
98 if isinstance(op_index, (int)) or op_index.isdigit():
99 op_index = int(op_index)
100 if op_index > 0 and op_index < len(ops) and ops[op_index] and ops[op_index]["detailed-status"]:
101 return ops[op_index]["detailed-status"]
102 # operation index is either non-numeric or out-of-range
103 return 'Unexpected error when getting detailed-status!'
104 else:
105 # Operations are NOT supported
106 return resp.get('_admin', {}).get('detailed-status')
107
108 def _has_delete_error(resp, entity, deleteFlag, delete_attempts_left):
109 if deleteFlag and delete_attempts_left:
110 state = _get_operational_state(resp, entity)
111 if state and state == 'ERROR':
112 return True
113 return False
114
115 def wait_for_status(entity_label, entity_id, timeout, apiUrlStatus, http_cmd, deleteFlag=False):
116 # Arguments:
117 # entity_label: String describing the entities using '--wait':
118 # 'NS', 'NSI', 'SDNC', 'VIM', 'WIM'
119 # entity_id: The ID for an existing entity, the operation ID for an entity to create.
120 # timeout: See section at top of this file for each value of TIMEOUT_<ENTITY>_OPERATION
121 # apiUrlStatus: The endpoint to get the Response including 'detailed-status'
122 # http_cmd: callback to HTTP command.
123 # Passing this callback as an argument avoids importing the 'http' module here.
124
125 # Loop here until the operation finishes, or a timeout occurs.
126 time_left = timeout
127 detailed_status = None
128 detailed_status_deleted = None
129 time_to_return = False
130 delete_attempts_left = MAX_DELETE_ATTEMPTS
131 wait_for_404 = False
132 try:
133 while True:
134 http_code, resp_unicode = http_cmd('{}/{}'.format(apiUrlStatus, entity_id))
135 resp = ''
136 if resp_unicode:
137 resp = json.loads(resp_unicode)
138 # print('HTTP CODE: {}'.format(http_code))
139 # print('RESP: {}'.format(resp))
140 # print('URL: {}/{}'.format(apiUrlStatus, entity_id))
141 if deleteFlag and http_code == 404:
142 # In case of deletion, '404 Not Found' means successfully deleted
143 # Display 'detailed-status: Deleted' and return
144 time_to_return = True
145 detailed_status_deleted = 'Deleted'
146 elif deleteFlag and http_code in (200, 201, 202, 204):
147 # In case of deletion and HTTP Status = 20* OK, deletion may be PROCESSING or COMPLETED
148 # If this is the case, we should keep on polling until 404 (deleted) is returned.
149 wait_for_404 = True
150 elif http_code not in (200, 201, 202, 204):
151 raise ClientException(str(resp))
152 if not time_to_return:
153 # Get operation status
154 op_status = _op_has_finished(resp, entity_label)
155 if op_status == -1:
156 # An error occurred
157 raise ClientException('unexpected response from server - {} '.format(
158 str(resp)))
159 elif op_status == 0:
160 # If there was an error upon deletion, try again to delete the same instance
161 # If the error is the same, there is probably nothing we can do but exit with error.
162 # If the error is different (i.e. 404), the instance was probably already corrupt, that is,
163 # operation(al)State was probably ERROR before deletion.
164 # In such a case, even if the previous state was ERROR, the deletion was successful,
165 # so detailed-status should be set to Deleted.
166 if _has_delete_error(resp, entity_label, deleteFlag, delete_attempts_left):
167 delete_attempts_left -= 1
168 else:
169 # Operation has finished, either with success or error
170 if deleteFlag:
171 delete_attempts_left -= 1
172 if not wait_for_404 and delete_attempts_left < MAX_DELETE_ATTEMPTS:
173 time_to_return = True
174 else:
175 time_to_return = True
176 new_detailed_status = _get_detailed_status(resp, entity_label, detailed_status_deleted)
177 # print('DETAILED-STATUS: {}'.format(new_detailed_status))
178 # print('DELETE-ATTEMPTS-LEFT: {}'.format(delete_attempts_left))
179 if not new_detailed_status:
180 new_detailed_status = 'In progress'
181 # TODO: Change LCM to provide detailed-status more up to date
182 # At the moment of this writing, 'detailed-status' may return different strings
183 # from different resources:
184 # /nslcm/v1/ns_lcm_op_occs/<id> ---> ''
185 # /nslcm/v1/ns_instances_content/<id> ---> 'deleting charms'
186 detailed_status = _show_detailed_status(detailed_status, new_detailed_status)
187 if time_to_return:
188 return
189 time_left -= POLLING_TIME_INTERVAL
190 sleep(POLLING_TIME_INTERVAL)
191 if time_left <= 0:
192 # There was a timeout, so raise an exception
193 raise ClientException('operation timeout, waited for {} seconds'.format(timeout))
194 except ClientException as exc:
195 message="Operation failed for {}:\nerror:\n{}".format(
196 entity_label,
197 str(exc))
198 raise ClientException(message)