Skip to content

Commit 25d1987

Browse files
committed
components class added
1 parent 23475df commit 25d1987

File tree

11 files changed

+310
-24
lines changed

11 files changed

+310
-24
lines changed
270 Bytes
Binary file not shown.
-802 Bytes
Binary file not shown.
16.4 KB
Binary file not shown.

cloudshell/sandbox_rest/exceptions.py

Lines changed: 0 additions & 13 deletions
This file was deleted.
File renamed without changes.

cloudshell/sandbox_rest/helpers/concurrency_helper.py

Whitespace-only changes.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Helper functions to poll the state of sandbox setup or the execution status of a command.
3+
Using 'retrying' library to do polling:
4+
https://pypi.org/project/retrying/
5+
"""
6+
from typing import List, Callable
7+
from retrying import retry # pip install retrying
8+
from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiClient
9+
from enum import Enum
10+
11+
12+
class SandboxStates(Enum):
13+
before_setup_state = "BeforeSetup"
14+
running_setup_state = "Setup"
15+
error_state = "Error"
16+
ready_state = "Ready"
17+
teardown_state = "Teardown"
18+
ended_state = "Ended"
19+
20+
21+
class ExecutionStatuses(Enum):
22+
running_status = "Running"
23+
pending_status = "Pending"
24+
completed_status = "Completed"
25+
failed_status = "Failed"
26+
27+
28+
UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value,
29+
ExecutionStatuses.pending_status.value]
30+
31+
32+
def _should_we_keep_polling_setup(sandbox_details: dict) -> bool:
33+
""" if still in setup keep polling """
34+
setup_states = [SandboxStates.before_setup_state.value, SandboxStates.running_setup_state.value]
35+
if sandbox_details["state"] in setup_states:
36+
return True
37+
return False
38+
39+
40+
def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool:
41+
""" if in teardown keep polling """
42+
if sandbox_details["state"] == SandboxStates.teardown_state.value:
43+
return True
44+
return False
45+
46+
47+
def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_func: Callable,
48+
max_polling_minutes: int, polling_frequency_seconds: int) -> str:
49+
""" Create blocking polling process """
50+
51+
# retry wait times are in milliseconds
52+
@retry(retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000,
53+
stop_max_delay=max_polling_minutes * 60000)
54+
def get_sandbox_details():
55+
return api.get_sandbox_details(reservation_id)
56+
57+
return get_sandbox_details()
58+
59+
60+
def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
61+
polling_frequency_seconds=30) -> dict:
62+
""" poll until completion """
63+
sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes,
64+
polling_frequency_seconds)
65+
return sandbox_details
66+
67+
68+
def poll_sandbox_teardown(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
69+
polling_frequency_seconds=30) -> dict:
70+
""" poll until completion """
71+
sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes,
72+
polling_frequency_seconds)
73+
return sandbox_details
74+
75+
76+
def _should_we_keep_polling_execution(exc_data: dict) -> bool:
77+
current_exc_status = exc_data["status"]
78+
if current_exc_status in UNFINISHED_EXECUTION_STATUSES:
79+
return True
80+
return False
81+
82+
83+
def poll_execution_for_completion(sandbox_rest: SandboxRestApiClient, command_execution_id: str,
84+
max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str:
85+
"""
86+
poll execution for "Completed" status, then return the execution output
87+
"""
88+
# retry wait times are in milliseconds
89+
@retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000,
90+
stop_max_delay=max_polling_in_minutes * 60000)
91+
def get_execution_data():
92+
exc_data = sandbox_rest.get_execution_details(command_execution_id)
93+
return exc_data
94+
return get_execution_data(sandbox_rest, command_execution_id)
95+
96+

cloudshell/sandbox_rest/sandbox_api.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@
33
from typing import List
44
import requests
55
from dataclasses import dataclass, asdict
6-
from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException
6+
7+
8+
class SandboxRestException(Exception):
9+
""" General exception to raise inside Rest client class """
10+
pass
11+
12+
13+
class SandboxRestAuthException(Exception):
14+
""" Failed auth actions """
15+
pass
16+
17+
18+
class SandboxRestInitException(ValueError):
19+
""" Failed auth actions """
20+
pass
721

822

923
@dataclass
@@ -16,7 +30,7 @@ class InputParam:
1630
value: str
1731

1832

19-
class SandboxRestApiClient:
33+
class SandboxRestApiSession:
2034
"""
2135
Python wrapper for CloudShell Sandbox API
2236
View http://<API_SERVER>/api/v2/explore to see schemas of return json values
@@ -207,7 +221,6 @@ def stop_sandbox(self, sandbox_id: str):
207221
response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers)
208222
if not response.ok:
209223
raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
210-
return response.json()
211224

212225
# SANDBOX GET REQUESTS
213226
def get_sandboxes(self, show_historic=False):
@@ -405,15 +418,17 @@ def delete_execution(self, execution_id: str):
405418
ADMIN_USER = "admin"
406419
ADMIN_PASS = "admin"
407420

408-
admin_api = SandboxRestApiClient(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS)
421+
admin_api = SandboxRestApiSession(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS)
409422
with admin_api:
410423
# sample api call with admin user session to get all sandboxes
411-
all_sandboxes = admin_api.get_sandboxes()
412-
print("== List of sandboxes pulled by Admin ===")
413-
print(json.dumps(all_sandboxes, indent=4))
424+
# all_sandboxes = admin_api.get_sandboxes()
425+
# print("== List of sandboxes pulled by Admin ===")
426+
# print(json.dumps(all_sandboxes, indent=4))
427+
428+
sb_res = admin_api.start_sandbox(blueprint_id="rest test", sandbox_name="rest test")
429+
print(sb_res["state"])
430+
time.sleep(4)
431+
414432

415-
admin_api.refresh_auth()
416-
all_sandboxes = admin_api.get_sandboxes()
417-
pass
418433

419434

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import json
2+
from typing import List
3+
from enum import Enum
4+
5+
6+
class ComponentTypes(Enum):
7+
app_type = "Application"
8+
resource_type = "Resource"
9+
service_type = "Service"
10+
11+
12+
class AppLifeCycleTypes(Enum):
13+
deployed = "Deployed"
14+
un_deployed = "Undeployed"
15+
16+
17+
class AttributeTypes(Enum):
18+
boolean_type = "boolean"
19+
password_type = "password"
20+
string_type = "string"
21+
numeric_type = "numeric"
22+
23+
24+
class SandboxRestComponents:
25+
def __init__(self, components: List[dict] = None):
26+
"""
27+
if instantiated with components then populate lists
28+
if not, then call "refresh_components" after getting data from sandbox
29+
"""
30+
self.all_components = components if components else []
31+
self.services = []
32+
self.resources = []
33+
self.deployed_apps = []
34+
self.un_deployed_apps = []
35+
if self.all_components:
36+
self._sort_components()
37+
38+
def _filter_components_by_type(self, component_type: str) -> List[dict]:
39+
""" accepts both short info components and full info """
40+
return [component for component in self.all_components if component["type"] == component_type]
41+
42+
def _filter_app_by_lifecycle(self, lifecycle_type):
43+
return [component for component in self.all_components
44+
if component["type"] == ComponentTypes.app_type.value
45+
and component["app_lifecyle"] == lifecycle_type]
46+
47+
def _sort_components(self) -> None:
48+
""" sort stored components into separate lists """
49+
self.resources = self._filter_components_by_type(ComponentTypes.resource_type.value)
50+
self.services = self._filter_components_by_type(ComponentTypes.service_type.value)
51+
self.deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.deployed.value)
52+
self.un_deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.un_deployed.value)
53+
54+
def refresh_components(self, components: List[dict]) -> None:
55+
self.all_components = components
56+
self._sort_components()
57+
58+
def find_component_by_name(self, component_name: str) -> dict:
59+
for component in self.all_components:
60+
if component["name"] == component_name:
61+
return component
62+
63+
@staticmethod
64+
def filter_by_model(components: List[dict], model: str) -> List[dict]:
65+
"""
66+
Can pass in all components or sub list
67+
Both Resources and Applications can use same shell / model
68+
"""
69+
return [component for component in components if component["component_type"] == model]
70+
71+
def filter_by_attr_value(self, components: List[dict], attribute_name: str, attribute_value: str) -> List[dict]:
72+
""" attribute name does not have to include model / family namespace """
73+
self._validate_components_for_attributes(components)
74+
result = []
75+
for component in components:
76+
for attr in component["attributes"]:
77+
if attr["name"].endswith(attribute_name) and attr["value"] == attribute_value:
78+
result.append(component)
79+
return result
80+
81+
def filter_by_boolean_attr_true(self, components: List[dict], attribute_name: str) -> List[dict]:
82+
""" attribute name does not have to include model / family namespace """
83+
self._validate_components_for_attributes(components)
84+
result = []
85+
for component in components:
86+
for attr in component["attributes"]:
87+
if attr["name"].endswith(attribute_name) and attr["value"].lower() == "true":
88+
result.append(component)
89+
return result
90+
91+
@staticmethod
92+
def _validate_components_for_attributes(components: List[dict]):
93+
if not components:
94+
return
95+
attrs = components[0].get("attributes")
96+
if not attrs:
97+
raise Exception("'attributes' member not found. Must pass in Full info components.\n"
98+
f"components data passed:\n{json.dumps(components, indent=4)}")
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import json
2+
import logging
3+
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+
8+
9+
class SandboxSetupError(Exception):
10+
""" When sandbox has error during setup """
11+
pass
12+
13+
14+
class SandboxTeardownError(Exception):
15+
""" When sandbox has error during setup """
16+
pass
17+
18+
19+
class CommandFailedException(Exception):
20+
pass
21+
22+
23+
# TODO:
24+
# add context manager methods
25+
# review design of class
26+
# add logger
27+
# publish env vars
28+
29+
30+
class SandboxRestController:
31+
def __init__(self, api: SandboxRestApiSession, sandbox_id="", log_file_handler=False):
32+
self.api = api
33+
self.sandbox_id = sandbox_id
34+
self.logger = None
35+
36+
def __enter__(self):
37+
return self
38+
39+
def __exit__(self, exc_type, exc_val, exc_tb):
40+
pass
41+
42+
def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M",
43+
bp_params: List[InputParam] = None, permitted_users: List[str] = None,
44+
max_polling_minutes=30) -> dict:
45+
""" 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)
50+
51+
sandbox_state = sandbox_details["state"]
52+
stage = sandbox_details["setup_stage"]
53+
name = sandbox_details["name"]
54+
55+
if "error" in sandbox_state.lower():
56+
activity_errors = self._get_all_activity_errors(sb_id)
57+
58+
# TODO: print this? log it? or dump into exception message?
59+
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)
65+
return sandbox_details
66+
67+
def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30) -> dict:
68+
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)
71+
sandbox_state = sandbox_details["state"]
72+
stage = sandbox_details["setup_stage"]
73+
name = sandbox_details["name"]
74+
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)
82+
return sandbox_details
83+
84+
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"]
87+
88+
def publish_sandbox_id_to_env_vars(self):
89+
pass

0 commit comments

Comments
 (0)