Coverage for osmclient/common/wait.py: 23%

86 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2024-06-22 09:01 +0000

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""" 

18OSM API handling for the '--wait' option 

19""" 

20 

21from osmclient.common.exceptions import ClientException, NotFound 

22import json 

23from time import sleep, time 

24from sys import stderr 

25 

26# Declare a constant for each module, to allow customizing each timeout in the future 

27TIMEOUT_GENERIC_OPERATION = 600 

28TIMEOUT_NSI_OPERATION = TIMEOUT_GENERIC_OPERATION 

29TIMEOUT_SDNC_OPERATION = TIMEOUT_GENERIC_OPERATION 

30TIMEOUT_VIM_OPERATION = TIMEOUT_GENERIC_OPERATION 

31TIMEOUT_K8S_OPERATION = TIMEOUT_GENERIC_OPERATION 

32TIMEOUT_WIM_OPERATION = TIMEOUT_GENERIC_OPERATION 

33TIMEOUT_NS_OPERATION = 3600 

34POLLING_TIME_INTERVAL = 5 

35MAX_DELETE_ATTEMPTS = 3 

36 

37 

38def _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 stderr.write("detailed-status: {}\n".format(new_detailed_status)) 

41 return new_detailed_status 

42 else: 

43 return old_detailed_status 

44 

45 

46def _get_finished_states(entity): 

47 """ 

48 Member name is either: 

49 operationState' (NS, NSI) 

50 '_admin.'operationalState' (VIM, WIM, SDN) 

51 For NS and NSI, 'operationState' may be one of: 

52 PROCESSING, COMPLETED,PARTIALLY_COMPLETED, FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK 

53 For VIM, WIM, SDN: '_admin.operationalState' may be one of: 

54 ENABLED, DISABLED, ERROR, PROCESSING 

55 

56 :param entity: can be NS, NSI, or other 

57 :return: two tuples with status completed strings, status failed string 

58 """ 

59 if entity == "NS" or entity == "NSI": 

60 return ("COMPLETED", "PARTIALLY_COMPLETED"), ("FAILED_TEMP", "FAILED") 

61 elif entity == "OPCANCEL": 

62 return ("FAILED_TEMP"), ("COMPLETED",) 

63 else: 

64 return ("ENABLED",), ("ERROR",) 

65 

66 

67def _get_operational_state(resp, entity): 

68 """ 

69 The member name is either: 

70 'operationState' (NS) 

71 'operational-status' (NSI) 

72 '_admin.'operationalState' (other) 

73 :param resp: descriptor of the get response 

74 :param entity: can be NS, NSI, or other 

75 :return: status of the operation 

76 """ 

77 if entity in ["NS", "NSI", "OPCANCEL"]: 

78 return resp.get("operationState") 

79 else: 

80 return resp.get("_admin", {}).get("operationalState") 

81 

82 

83def _op_has_finished(resp, entity): 

84 """ 

85 Indicates if operation has finished ok or is processing 

86 :param resp: descriptor of the get response 

87 :param entity: can be NS, NSI, or other 

88 :return: 

89 True on success (operation has finished) 

90 False on pending (operation has not finished) 

91 raise Exception if unexpected response, or ended with error 

92 """ 

93 finished_states_ok, finished_states_error = _get_finished_states(entity) 

94 if resp: 

95 op_state = _get_operational_state(resp, entity) 

96 if op_state: 

97 if op_state in finished_states_ok: 

98 return True 

99 elif op_state in finished_states_error: 

100 raise ClientException( 

101 "Operation failed with status '{}'".format(op_state) 

102 ) 

103 return False 

104 raise ClientException("Unexpected response from server: {} ".format(resp)) 

105 

106 

107def _get_detailed_status(resp, entity): 

108 """ 

109 For VIM, WIM, SDN, 'detailed-status' is either: 

110 - a leaf node to '_admin' (operations NOT supported) 

111 - a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI) 

112 :param resp: content of the get response 

113 :param entity: can be NS, NSI, or other 

114 :return: 

115 """ 

116 if entity in ("NS", "NSI", "OPCANCEL"): 

117 # For NS and NSI, 'detailed-status' is a JSON "root" member: 

118 return resp.get("detailed-status") 

119 else: 

120 ops = resp.get("_admin", {}).get("operations") 

121 current_op = resp.get("_admin", {}).get("current_operation") 

122 if ops and current_op is not None: 

123 # Operations are supported, verify operation index 

124 if isinstance(ops, dict) and current_op in ops: 

125 return ops[current_op].get("detailed-status") 

126 elif ( 

127 isinstance(ops, list) 

128 and isinstance(current_op, int) 

129 or current_op.isdigit() 

130 ): 

131 current_op = int(current_op) 

132 if ( 

133 current_op >= 0 

134 and current_op < len(ops) 

135 and ops[current_op] 

136 and ops[current_op]["detailed-status"] 

137 ): 

138 return ops[current_op]["detailed-status"] 

139 # operation index is either non-numeric or out-of-range 

140 return "Unexpected error when getting detailed-status!" 

141 else: 

142 # Operations are NOT supported 

143 return resp.get("_admin", {}).get("detailed-status") 

144 

145 

146def wait_for_status( 

147 entity_label, entity_id, timeout, apiUrlStatus, http_cmd, deleteFlag=False 

148): 

149 """ 

150 Wait until operation ends, making polling every 5s. Prints detailed status when it changes 

151 :param entity_label: String describing the entities using '--wait': 'NS', 'NSI', 'SDNC', 'VIM', 'WIM' 

152 :param entity_id: The ID for an existing entity, the operation ID for an entity to create. 

153 :param timeout: Timeout in seconds 

154 :param apiUrlStatus: The endpoint to get the Response including 'detailed-status' 

155 :param http_cmd: callback to HTTP command. (Normally the get method) 

156 :param deleteFlag: If this is a delete operation 

157 :return: None, exception if operation fails or timeout 

158 """ 

159 

160 # Loop here until the operation finishes, or a timeout occurs. 

161 time_to_finish = time() + timeout 

162 detailed_status = None 

163 retries = 0 

164 max_retries = 1 

165 while True: 

166 try: 

167 http_code, resp_unicode = http_cmd("{}/{}".format(apiUrlStatus, entity_id)) 

168 retries = 0 

169 except NotFound: 

170 if deleteFlag: 

171 _show_detailed_status(detailed_status, "Deleted") 

172 return 

173 raise 

174 except ClientException: 

175 if retries >= max_retries or time() < time_to_finish: 

176 raise 

177 retries += 1 

178 sleep(POLLING_TIME_INTERVAL) 

179 continue 

180 

181 resp = "" 

182 if resp_unicode: 

183 resp = json.loads(resp_unicode) 

184 

185 new_detailed_status = _get_detailed_status(resp, entity_label) 

186 # print('DETAILED-STATUS: {}'.format(new_detailed_status)) 

187 if not new_detailed_status: 

188 new_detailed_status = "In progress" 

189 detailed_status = _show_detailed_status(detailed_status, new_detailed_status) 

190 

191 # Get operation status 

192 if _op_has_finished(resp, entity_label): 

193 return 

194 

195 if time() >= time_to_finish: 

196 # There was a timeout, so raise an exception 

197 raise ClientException("operation timeout after {} seconds".format(timeout)) 

198 sleep(POLLING_TIME_INTERVAL)