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 |
0 |
""" |
18 |
|
OSM API handling for the '--wait' option |
19 |
|
""" |
20 |
|
|
21 |
0 |
from osmclient.common.exceptions import ClientException, NotFound |
22 |
0 |
import json |
23 |
0 |
from time import sleep, time |
24 |
0 |
from sys import stderr |
25 |
|
|
26 |
|
# Declare a constant for each module, to allow customizing each timeout in the future |
27 |
0 |
TIMEOUT_GENERIC_OPERATION = 600 |
28 |
0 |
TIMEOUT_NSI_OPERATION = TIMEOUT_GENERIC_OPERATION |
29 |
0 |
TIMEOUT_SDNC_OPERATION = TIMEOUT_GENERIC_OPERATION |
30 |
0 |
TIMEOUT_VIM_OPERATION = TIMEOUT_GENERIC_OPERATION |
31 |
0 |
TIMEOUT_K8S_OPERATION = TIMEOUT_GENERIC_OPERATION |
32 |
0 |
TIMEOUT_WIM_OPERATION = TIMEOUT_GENERIC_OPERATION |
33 |
0 |
TIMEOUT_NS_OPERATION = 3600 |
34 |
0 |
POLLING_TIME_INTERVAL = 5 |
35 |
0 |
MAX_DELETE_ATTEMPTS = 3 |
36 |
|
|
37 |
|
|
38 |
0 |
def _show_detailed_status(old_detailed_status, new_detailed_status): |
39 |
0 |
if new_detailed_status is not None and new_detailed_status != old_detailed_status: |
40 |
0 |
stderr.write("detailed-status: {}\n".format(new_detailed_status)) |
41 |
0 |
return new_detailed_status |
42 |
|
else: |
43 |
0 |
return old_detailed_status |
44 |
|
|
45 |
|
|
46 |
0 |
def _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 |
0 |
if entity == 'NS' or entity == 'NSI': |
60 |
0 |
return ('COMPLETED', 'PARTIALLY_COMPLETED'), ('FAILED_TEMP', 'FAILED') |
61 |
|
else: |
62 |
0 |
return ('ENABLED', ), ('ERROR', ) |
63 |
|
|
64 |
|
|
65 |
0 |
def _get_operational_state(resp, entity): |
66 |
|
""" |
67 |
|
The member name is either: |
68 |
|
'operationState' (NS) |
69 |
|
'operational-status' (NSI) |
70 |
|
'_admin.'operationalState' (other) |
71 |
|
:param resp: descriptor of the get response |
72 |
|
:param entity: can be NS, NSI, or other |
73 |
|
:return: status of the operation |
74 |
|
""" |
75 |
0 |
if entity == 'NS' or entity == 'NSI': |
76 |
0 |
return resp.get('operationState') |
77 |
|
else: |
78 |
0 |
return resp.get('_admin', {}).get('operationalState') |
79 |
|
|
80 |
|
|
81 |
0 |
def _op_has_finished(resp, entity): |
82 |
|
""" |
83 |
|
Indicates if operation has finished ok or is processing |
84 |
|
:param resp: descriptor of the get response |
85 |
|
:param entity: can be NS, NSI, or other |
86 |
|
:return: |
87 |
|
True on success (operation has finished) |
88 |
|
False on pending (operation has not finished) |
89 |
|
raise Exception if unexpected response, or ended with error |
90 |
|
""" |
91 |
0 |
finished_states_ok, finished_states_error = _get_finished_states(entity) |
92 |
0 |
if resp: |
93 |
0 |
op_state = _get_operational_state(resp, entity) |
94 |
0 |
if op_state: |
95 |
0 |
if op_state in finished_states_ok: |
96 |
0 |
return True |
97 |
0 |
elif op_state in finished_states_error: |
98 |
0 |
raise ClientException("Operation failed with status '{}'".format(op_state)) |
99 |
0 |
return False |
100 |
0 |
raise ClientException('Unexpected response from server: {} '.format(resp)) |
101 |
|
|
102 |
|
|
103 |
0 |
def _get_detailed_status(resp, entity): |
104 |
|
""" |
105 |
|
For VIM, WIM, SDN, 'detailed-status' is either: |
106 |
|
- a leaf node to '_admin' (operations NOT supported) |
107 |
|
- a leaf node of the Nth element in the list '_admin.operations[]' (operations supported by LCM and NBI) |
108 |
|
:param resp: content of the get response |
109 |
|
:param entity: can be NS, NSI, or other |
110 |
|
:return: |
111 |
|
""" |
112 |
0 |
if entity in ('NS', 'NSI'): |
113 |
|
# For NS and NSI, 'detailed-status' is a JSON "root" member: |
114 |
0 |
return resp.get('detailed-status') |
115 |
|
else: |
116 |
0 |
ops = resp.get('_admin', {}).get('operations') |
117 |
0 |
current_op = resp.get('_admin', {}).get('current_operation') |
118 |
0 |
if ops and current_op is not None: |
119 |
|
# Operations are supported, verify operation index |
120 |
0 |
if isinstance(ops, dict) and current_op in ops: |
121 |
0 |
return ops[current_op].get("detailed-status") |
122 |
0 |
elif isinstance(ops, list) and isinstance(current_op, int) or current_op.isdigit(): |
123 |
0 |
current_op = int(current_op) |
124 |
0 |
if current_op >= 0 and current_op < len(ops) and ops[current_op] and ops[current_op]["detailed-status"]: |
125 |
0 |
return ops[current_op]["detailed-status"] |
126 |
|
# operation index is either non-numeric or out-of-range |
127 |
0 |
return 'Unexpected error when getting detailed-status!' |
128 |
|
else: |
129 |
|
# Operations are NOT supported |
130 |
0 |
return resp.get('_admin', {}).get('detailed-status') |
131 |
|
|
132 |
|
|
133 |
0 |
def wait_for_status(entity_label, entity_id, timeout, apiUrlStatus, http_cmd, deleteFlag=False): |
134 |
|
""" |
135 |
|
Wait until operation ends, making polling every 5s. Prints detailed status when it changes |
136 |
|
:param entity_label: String describing the entities using '--wait': 'NS', 'NSI', 'SDNC', 'VIM', 'WIM' |
137 |
|
:param entity_id: The ID for an existing entity, the operation ID for an entity to create. |
138 |
|
:param timeout: Timeout in seconds |
139 |
|
:param apiUrlStatus: The endpoint to get the Response including 'detailed-status' |
140 |
|
:param http_cmd: callback to HTTP command. (Normally the get method) |
141 |
|
:param deleteFlag: If this is a delete operation |
142 |
|
:return: None, exception if operation fails or timeout |
143 |
|
""" |
144 |
|
|
145 |
|
# Loop here until the operation finishes, or a timeout occurs. |
146 |
0 |
time_to_finish = time() + timeout |
147 |
0 |
detailed_status = None |
148 |
0 |
retries = 0 |
149 |
0 |
max_retries = 1 |
150 |
0 |
while True: |
151 |
0 |
try: |
152 |
0 |
http_code, resp_unicode = http_cmd('{}/{}'.format(apiUrlStatus, entity_id)) |
153 |
0 |
retries = 0 |
154 |
0 |
except NotFound: |
155 |
0 |
if deleteFlag: |
156 |
0 |
_show_detailed_status(detailed_status, 'Deleted') |
157 |
0 |
return |
158 |
0 |
raise |
159 |
0 |
except ClientException: |
160 |
0 |
if retries >= max_retries or time() < time_to_finish: |
161 |
0 |
raise |
162 |
0 |
retries += 1 |
163 |
0 |
sleep(POLLING_TIME_INTERVAL) |
164 |
0 |
continue |
165 |
|
|
166 |
0 |
resp = '' |
167 |
0 |
if resp_unicode: |
168 |
0 |
resp = json.loads(resp_unicode) |
169 |
|
|
170 |
0 |
new_detailed_status = _get_detailed_status(resp, entity_label) |
171 |
|
# print('DETAILED-STATUS: {}'.format(new_detailed_status)) |
172 |
0 |
if not new_detailed_status: |
173 |
0 |
new_detailed_status = 'In progress' |
174 |
0 |
detailed_status = _show_detailed_status(detailed_status, new_detailed_status) |
175 |
|
|
176 |
|
# Get operation status |
177 |
0 |
if _op_has_finished(resp, entity_label): |
178 |
0 |
return |
179 |
|
|
180 |
0 |
if time() >= time_to_finish: |
181 |
|
# There was a timeout, so raise an exception |
182 |
0 |
raise ClientException('operation timeout after {} seconds'.format(timeout)) |
183 |
0 |
sleep(POLLING_TIME_INTERVAL) |