Skip to content

Commit e722362

Browse files
committed
added state management to start / stop commands
1 parent 25d1987 commit e722362

File tree

7 files changed

+146
-48
lines changed

7 files changed

+146
-48
lines changed
462 Bytes
Binary file not shown.
Binary file not shown.
191 Bytes
Binary file not shown.
Binary file not shown.

cloudshell/sandbox_rest/helpers/concurrency_helper.py

Whitespace-only changes.

cloudshell/sandbox_rest/helpers/polling_helpers.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
https://pypi.org/project/retrying/
55
"""
66
from typing import List, Callable
7-
from retrying import retry # pip install retrying
8-
from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiClient
7+
from retrying import retry, RetryError # pip install retrying
8+
from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
99
from enum import Enum
1010

1111

12+
class OrchestrationPollingTimeout(Exception):
13+
pass
14+
15+
16+
class CommandPollingTimeout(Exception):
17+
pass
18+
19+
1220
class SandboxStates(Enum):
1321
before_setup_state = "BeforeSetup"
1422
running_setup_state = "Setup"
@@ -25,6 +33,12 @@ class ExecutionStatuses(Enum):
2533
failed_status = "Failed"
2634

2735

36+
SANDBOX_SETUP_STATES = [SandboxStates.before_setup_state.value,
37+
SandboxStates.running_setup_state.value]
38+
39+
SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value,
40+
SandboxStates.error_state.value]
41+
2842
UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value,
2943
ExecutionStatuses.pending_status.value]
3044

@@ -44,7 +58,7 @@ def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool:
4458
return False
4559

4660

47-
def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_func: Callable,
61+
def _poll_sandbox_state(api: SandboxRestApiSession, reservation_id: str, polling_func: Callable,
4862
max_polling_minutes: int, polling_frequency_seconds: int) -> str:
4963
""" Create blocking polling process """
5064

@@ -54,18 +68,22 @@ def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_
5468
def get_sandbox_details():
5569
return api.get_sandbox_details(reservation_id)
5670

57-
return get_sandbox_details()
71+
try:
72+
sandbox_details = get_sandbox_details()
73+
except RetryError:
74+
raise OrchestrationPollingTimeout(f"Sandbox Polling timed out after configured {max_polling_minutes} minutes")
75+
return sandbox_details
5876

5977

60-
def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
78+
def poll_sandbox_setup(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20,
6179
polling_frequency_seconds=30) -> dict:
6280
""" poll until completion """
6381
sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes,
6482
polling_frequency_seconds)
6583
return sandbox_details
6684

6785

68-
def poll_sandbox_teardown(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
86+
def poll_sandbox_teardown(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20,
6987
polling_frequency_seconds=30) -> dict:
7088
""" poll until completion """
7189
sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes,
@@ -80,17 +98,21 @@ def _should_we_keep_polling_execution(exc_data: dict) -> bool:
8098
return False
8199

82100

83-
def poll_execution_for_completion(sandbox_rest: SandboxRestApiClient, command_execution_id: str,
101+
def poll_execution_for_completion(sandbox_rest: SandboxRestApiSession, command_execution_id: str,
84102
max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str:
85103
"""
86104
poll execution for "Completed" status, then return the execution output
87105
"""
106+
88107
# retry wait times are in milliseconds
89108
@retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000,
90109
stop_max_delay=max_polling_in_minutes * 60000)
91110
def get_execution_data():
92111
exc_data = sandbox_rest.get_execution_details(command_execution_id)
93112
return exc_data
94-
return get_execution_data(sandbox_rest, command_execution_id)
95-
96113

114+
try:
115+
exc_data = get_execution_data(sandbox_rest, command_execution_id)
116+
except RetryError:
117+
raise CommandPollingTimeout(f"Execution polling timed out after max {max_polling_in_minutes} minutes")
118+
return exc_data
Lines changed: 115 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam
2+
from cloudshell.sandbox_rest.helpers.polling_helpers import poll_sandbox_setup, poll_sandbox_teardown, \
3+
poll_execution_for_completion, SandboxStates, SANDBOX_SETUP_STATES, SANDBOX_ACTIVE_STATES
4+
from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
15
import json
2-
import logging
36
from typing import List
4-
5-
from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam
6-
from cloudshell.sandbox_rest.helpers import polling_helpers as poll_help
7+
from timeit import default_timer
78

89

910
class SandboxSetupError(Exception):
@@ -20,70 +21,145 @@ class CommandFailedException(Exception):
2021
pass
2122

2223

23-
# TODO:
24-
# add context manager methods
25-
# review design of class
26-
# add logger
27-
# publish env vars
28-
29-
3024
class SandboxRestController:
31-
def __init__(self, api: SandboxRestApiSession, sandbox_id="", log_file_handler=False):
25+
def __init__(self, api: SandboxRestApiSession, sandbox_id="", disable_prints=False):
3226
self.api = api
3327
self.sandbox_id = sandbox_id
34-
self.logger = None
28+
self.components = SandboxRestComponents()
29+
self.setup_finished = False
30+
self.sandbox_ended = False
31+
self.setup_errors: List[dict] = None
32+
self.teardown_errors: List[dict] = None
33+
self.disable_prints = disable_prints
34+
35+
# when passing in existing sandbox id update instance state, otherwise let "start sandbox" handle it
36+
if self.sandbox_id:
37+
self._handle_sandbox_id_on_init()
3538

3639
def __enter__(self):
3740
return self
3841

3942
def __exit__(self, exc_type, exc_val, exc_tb):
40-
pass
43+
if exc_type:
44+
err_msg = f"Exiting sandbox scope with error. f{exc_type}: {exc_val}"
45+
self._print(err_msg)
46+
47+
def _handle_sandbox_id_on_init(self):
48+
self.update_components()
49+
self.update_state_flags()
50+
51+
def update_components(self):
52+
components = self.api.get_sandbox_components(self.sandbox_id)
53+
self.components.refresh_components(components)
54+
55+
def update_state_flags(self):
56+
details = self.api.get_sandbox_details(self.sandbox_id)
57+
sandbox_state = details["state"]
58+
if sandbox_state not in SANDBOX_SETUP_STATES:
59+
self.setup_finished = True
60+
elif sandbox_state not in SANDBOX_ACTIVE_STATES:
61+
self.sandbox_ended = True
62+
63+
def _print(self, message):
64+
if not self.disable_prints:
65+
print(message)
4166

4267
def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M",
4368
bp_params: List[InputParam] = None, permitted_users: List[str] = None,
44-
max_polling_minutes=30) -> dict:
69+
max_polling_minutes=30, raise_setup_exception=True) -> dict:
4570
""" Start sandbox, poll for result, get back sandbox info """
46-
start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
47-
sb_id = start_response["id"]
48-
sandbox_details = poll_help.poll_sandbox_setup(self.api, sb_id, max_polling_minutes,
49-
polling_frequency_seconds=30)
71+
if self.sandbox_id:
72+
raise ValueError(f"Sandbox already has id '{self.sandbox_id}'.\n"
73+
f"Start blueprint with new sandbox controller instance")
5074

75+
self._print(f"Starting blueprint {blueprint_id}")
76+
start = default_timer()
77+
start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
78+
self.sandbox_id = start_response["id"]
79+
sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes,
80+
polling_frequency_seconds=30)
81+
self.update_components()
5182
sandbox_state = sandbox_details["state"]
5283
stage = sandbox_details["setup_stage"]
5384
name = sandbox_details["name"]
5485

55-
if "error" in sandbox_state.lower():
56-
activity_errors = self._get_all_activity_errors(sb_id)
86+
if sandbox_state == SandboxStates.ready_state.value:
87+
self.setup_finished = True
5788

58-
# TODO: print this? log it? or dump into exception message?
89+
# scan activity feed for errors
90+
if sandbox_state == SandboxStates.error_state.value:
91+
self.setup_finished = True
92+
activity_errors = self._get_all_activity_errors(self.sandbox_id)
5993
if activity_errors:
60-
print(f"=== Activity Feed Errors ===\n{json.dumps(activity_errors, indent=4)}")
61-
62-
err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
63-
f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sb_id}'")
64-
raise SandboxSetupError(err_msg)
94+
# print and store setup error data
95+
self.setup_errors = activity_errors
96+
err_msg = f"Error Events during setup:\n{json.dumps(activity_errors, indent=4)}"
97+
self._print(err_msg)
98+
99+
if raise_setup_exception:
100+
err_msg = (f"Sandbox '{name}' Error during SETUP. See events for details.\n"
101+
f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'")
102+
raise SandboxSetupError(err_msg)
103+
104+
total_minutes = (default_timer() - start) / 60
105+
self._print(f"Setup finished after {total_minutes:.2f} minutes.")
65106
return sandbox_details
66107

67-
def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30) -> dict:
108+
def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_teardown_exception=True) -> dict:
109+
if self.sandbox_ended:
110+
raise ValueError(f"sandbox {self.sandbox_id} already in completed state")
111+
112+
last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"]
113+
start = default_timer()
68114
self.api.stop_sandbox(sandbox_id)
69-
sandbox_details = poll_help.poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes,
70-
polling_frequency_seconds=30)
115+
sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes,
116+
polling_frequency_seconds=30)
117+
self.update_components()
71118
sandbox_state = sandbox_details["state"]
72119
stage = sandbox_details["setup_stage"]
73120
name = sandbox_details["name"]
74121

75-
if "error" in sandbox_state.lower():
76-
tail_error_count = 5
77-
tailed_errors = self.api.get_sandbox_activity(sandbox_id, error_only=True, tail=tail_error_count)["events"]
78-
print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
79-
err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
80-
f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'")
81-
raise SandboxTeardownError(err_msg)
122+
if sandbox_state == SandboxStates.ended_state.value:
123+
self.sandbox_ended = True
124+
125+
tail_error_count = 5
126+
tailed_errors = self.api.get_sandbox_activity(sandbox_id,
127+
error_only=True,
128+
from_event_id=last_activity_id + 1,
129+
tail=tail_error_count)["events"]
130+
if tailed_errors:
131+
self.sandbox_ended = True
132+
self.teardown_errors = tailed_errors
133+
self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
134+
135+
if raise_teardown_exception:
136+
err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
137+
f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'")
138+
raise SandboxTeardownError(err_msg)
139+
140+
total_minutes = (default_timer() - start) / 60
141+
self._print(f"Teardown done in {total_minutes:.2f} minutes.")
82142
return sandbox_details
83143

144+
def rerun_setup(self):
145+
pass
146+
147+
def run_sandbox_command_and_poll(self):
148+
pass
149+
150+
def run_component_command_and_poll(self):
151+
pass
152+
84153
def _get_all_activity_errors(self, sandbox_id):
85-
activity_results = self.api.get_sandbox_activity(sandbox_id, error_only=True)
86-
return activity_results["events"]
154+
return self.api.get_sandbox_activity(sandbox_id, error_only=True)["events"]
87155

88156
def publish_sandbox_id_to_env_vars(self):
157+
""" publish sandbox id as environment variable for different CI process to pick up """
89158
pass
159+
160+
161+
if __name__ == "__main__":
162+
api = SandboxRestApiSession("localhost", "admin", "admin")
163+
controller = SandboxRestController(api)
164+
response = controller.start_sandbox_and_poll("rest test", "lolol")
165+
print(json.dumps(response, indent=4))

0 commit comments

Comments
 (0)