9610856cc98587cf34b9918d576d2093bf82a2bc
[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_WIM_OPERATION = TIMEOUT_GENERIC_OPERATION
32 TIMEOUT_NS_OPERATION = 3600
33
34 POLLING_TIME_INTERVAL = 1
35
36 MAX_DELETE_ATTEMPTS = 3
37
38 def _show_detailed_status(old_detailed_status, new_detailed_status):
39 if new_detailed_status is not None and new_detailed_status != old_detailed_status:
40 sys.stderr.write("detailed-status: {}\n".format(new_detailed_status))
41 return new_detailed_status
42 else:
43 return old_detailed_status
44
45 def _get_finished_states(entity):
46 # Note that the member name is either:
47 # 'operationState' (NS and NSI)
48 # '_admin.'operationalState' (other)
49 # For NS and NSI, 'operationState' may be one of:
50 # PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK
51 # For other entities, '_admin.operationalState' may be one of:
52 # operationalState: ENABLED, DISABLED, ERROR, PROCESSING
53 if entity == 'NS' or entity == 'NSI':
54 return ['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED_TEMP', 'FAILED']
55 else:
56 return ['ENABLED', 'ERROR']
57
58 def _get_operational_state(resp, entity):
59 # Note that the member name is either:
60 # 'operationState' (NS)
61 # 'operational-status' (NSI)
62 # '_admin.'operationalState' (other)
63 if entity == 'NS' or entity == 'NSI':
64 return resp.get('operationState')
65 else:
66 return resp.get('_admin', {}).get('operationalState')
67
68 def _op_has_finished(resp, entity):
69 # _op_has_finished() returns:
70 # 0 on success (operation has finished)
71 # 1 on pending (operation has not finished)
72 # -1 on error (bad response)
73 #
74 finished_states = _get_finished_states(entity)
75 if resp:
76 operationalState = _get_operational_state(resp, entity)
77 if operationalState:
78 if operationalState in finished_states:
79 return 0
80 return 1
81 return -1
82
83 def _get_detailed_status(resp, entity, detailed_status_deleted):
84 if detailed_status_deleted:
85 return detailed_status_deleted
86 if entity == 'NS' or entity == 'NSI':
87 # For NS and NSI, 'detailed-status' is a JSON "root" member:
88 return resp.get('detailed-status')
89 else:
90 # For other entities, 'detailed-status' a leaf node to '_admin':
91 return resp.get('_admin', {}).get('detailed-status')
92
93 def _has_delete_error(resp, entity, deleteFlag, delete_attempts_left):
94 if deleteFlag and delete_attempts_left:
95 state = _get_operational_state(resp, entity)
96 if state and state == 'ERROR':
97 return True
98 return False
99
100 def wait_for_status(entity_label, entity_id, timeout, apiUrlStatus, http_cmd, deleteFlag=False):
101 # Arguments:
102 # entity_label: String describing the entities using '--wait':
103 # 'NS', 'NSI', 'SDNC', 'VIM', 'WIM'
104 # entity_id: The ID for an existing entity, the operation ID for an entity to create.
105 # timeout: See section at top of this file for each value of TIMEOUT_<ENTITY>_OPERATION
106 # apiUrlStatus: The endpoint to get the Response including 'detailed-status'
107 # http_cmd: callback to HTTP command.
108 # Passing this callback as an argument avoids importing the 'http' module here.
109
110 # Loop here until the operation finishes, or a timeout occurs.
111 time_left = timeout
112 detailed_status = None
113 detailed_status_deleted = None
114 time_to_return = False
115 delete_attempts_left = MAX_DELETE_ATTEMPTS
116 try:
117 while True:
118 http_code, resp_unicode = http_cmd('{}/{}'.format(apiUrlStatus, entity_id))
119 resp = ''
120 if resp_unicode:
121 resp = json.loads(resp_unicode)
122 # print 'HTTP CODE: {}'.format(http_code)
123 # print 'RESP: {}'.format(resp)
124 # print 'URL: {}/{}'.format(apiUrlStatus, entity_id)
125 if deleteFlag and http_code == 404:
126 # In case of deletion, '404 Not Found' means successfully deleted
127 # Display 'detailed-status: Deleted' and return
128 time_to_return = True
129 detailed_status_deleted = 'Deleted'
130 elif http_code not in (200, 201, 202, 204):
131 raise ClientException(str(resp))
132 if not time_to_return:
133 # Get operation status
134 op_status = _op_has_finished(resp, entity_label)
135 if op_status == -1:
136 # An error occurred
137 raise ClientException('unexpected response from server - {} '.format(
138 str(resp)))
139 elif op_status == 0:
140 # If there was an error upon deletion, try again to delete the same instance
141 # If the error is the same, there is probably nothing we can do but exit with error.
142 # If the error is different (i.e. 404), the instance was probably already corrupt, that is,
143 # operation(al)State was probably ERROR before deletion.
144 # In such a case, even if the previous state was ERROR, the deletion was successful,
145 # so detailed-status should be set to Deleted.
146 if _has_delete_error(resp, entity_label, deleteFlag, delete_attempts_left):
147 delete_attempts_left -= 1
148 else:
149 # Operation has finished, either with success or error
150 if deleteFlag:
151 if delete_attempts_left < MAX_DELETE_ATTEMPTS:
152 time_to_return = True
153 delete_attempts_left -= 1
154 else:
155 time_to_return = True
156 new_detailed_status = _get_detailed_status(resp, entity_label, detailed_status_deleted)
157 if not new_detailed_status:
158 new_detailed_status = 'In progress'
159 # TODO: Change LCM to provide detailed-status more up to date
160 # At the moment of this writing, 'detailed-status' may return different strings
161 # from different resources:
162 # /nslcm/v1/ns_lcm_op_occs/<id> ---> ''
163 # /nslcm/v1/ns_instances_content/<id> ---> 'deleting charms'
164 detailed_status = _show_detailed_status(detailed_status, new_detailed_status)
165 if time_to_return:
166 return
167 time_left -= POLLING_TIME_INTERVAL
168 sleep(POLLING_TIME_INTERVAL)
169 if time_left <= 0:
170 # There was a timeout, so raise an exception
171 raise ClientException('operation timeout, waited for {} seconds'.format(timeout))
172 except ClientException as exc:
173 message="Operation failed for {}:\nerror:\n{}".format(
174 entity_label,
175 exc.message)
176 raise ClientException(message)