From 81a3b75501eeb95697e3854141c1c23db4611f1b Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Fri, 1 Oct 2021 10:48:40 +0300 Subject: [PATCH 01/29] added gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file From 59eef4b2178518a9d31d6946c0edc0f237ba6797 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Fri, 1 Oct 2021 15:45:00 +0300 Subject: [PATCH 02/29] refactored all post requests --- Sandbox-API/Sandbox.py | 357 --------------------- Sandbox-API/quali_config.json | 7 - requirements.txt | 1 + {Sandbox-API => sandbox_api}/__init__.py | 0 sandbox_api/sandbox_rest.py | 381 +++++++++++++++++++++++ setup.py | 1 - 6 files changed, 382 insertions(+), 365 deletions(-) delete mode 100644 Sandbox-API/Sandbox.py delete mode 100644 Sandbox-API/quali_config.json create mode 100644 requirements.txt rename {Sandbox-API => sandbox_api}/__init__.py (100%) create mode 100644 sandbox_api/sandbox_rest.py diff --git a/Sandbox-API/Sandbox.py b/Sandbox-API/Sandbox.py deleted file mode 100644 index ee72f02..0000000 --- a/Sandbox-API/Sandbox.py +++ /dev/null @@ -1,357 +0,0 @@ -import json -import requests - - -class SandboxAPI: - """ Python wrapper for CloudShell Sandbox API - """ - def __init__(self, host, username, password, domain='Global', port=82): - """Initializes and logs in Sandbox - :param str host: hostname of IP address of sandbox API server - :param str username: CloudShell username - :param str password: CloudShell password - :param str domain: CloudShell domain (default=Global) - :param int port: Sandbox API port number(default=82) - """ - self._server_url = 'http://{}:{}/api'.format(host, port) - response = requests.put('{}/login'.format(self._server_url), - json={'username': username, 'password': password, 'domain': domain}) - - self._headers = {"Authorization": "Basic " + response.content[1:-1].decode('utf-8'), - "Content-Type": "application/json"} - - def get_blueprints(self): - """Get list of blueprints - :return: - """ - response = requests.get('{}/v2/blueprints'.format(self._server_url), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_blueprint_details(self, blueprint_id): - """Get details of a specific blueprint - :param blueprint_id: Blueprint name or id - :return: - """ - response = requests.get('{}/v2/blueprints/{}'.format(self._server_url, blueprint_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def start_sandbox(self, blueprint_id, duration, sandbox_name=None, parameters=None, permitted_users=None): - """Create a sandbox from the provided blueprint id - :param list permitted_users: list of permitted users ex: ['user1', 'user2'] - :param list parameters: List of dicts, input parameters in the format ex: [{"name": "Version", - "value": "3.0"}, {"name": "Build Number", "value": "5"}] - :param str blueprint_id: blueprint_id or name - :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) - :param str sandbox_name: name of the sandbox, same as blueprint if name='' - :return: - """ - if not sandbox_name: - sandbox_name = self.get_blueprint_details(blueprint_id)['name'] - data_dict = {"duration": duration, "name": sandbox_name} - if permitted_users: - data_dict['permitted_users'] = permitted_users - if parameters: - data_dict["params"] = parameters - - response = requests.post('{}/v2/blueprints/{}/start'.format(self._server_url, blueprint_id), - headers=self._headers, - data=json.dumps(data_dict)) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandboxes(self): - """Get list of sandboxes - :return: - """ - response = requests.get('{}/v2/sandboxes'.format(self._server_url), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_details(self, sandbox_id): - """Get details of the given sandbox id - :param sandbox_id: Sandbox id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}'.format(self._server_url, sandbox_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_activity(self, sandbox_id): - """Get list of sandbox activity - :param str sandbox_id: Sandbox id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._server_url, sandbox_id, 'activity'), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_commands(self, sandbox_id): - """Get list of sandbox commands - :param str sandbox_id: Sandbox id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/commands'.format(self._server_url, sandbox_id), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_command_details(self, sandbox_id, command_name): - """Get details of specific sandbox command - :param str sandbox_id: Sandbox id - :param str command_name: Sandbox command to be executed - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format( - self._server_url, sandbox_id, command_name), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def sandbox_command_start(self, sandbox_id, command_name, params=None): - """Start a sandbox command - :param str sandbox_id: Sandbox id - :param str command_name: Sandbox command to be executed - :param dict params: parameters to be passed to the command ex: {"params": [{"name": "WaitTime", "value": "1"}], - "printOutput": True} - :return: - """ - if params: - response = requests.post('{}/v2/sandboxes/{}/commands/{}/start'.format( - self._server_url, sandbox_id, command_name), - data=json.dumps(params), - headers=self._headers) - else: - response = requests.post('{}/v2/sandboxes/{}/commands/{}/start'.format( - self._server_url, sandbox_id, command_name), - headers=self._headers) - - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_components(self, sandbox_id): - """Get list of sandbox components - :param str sandbox_id: Sandbox id - :return: Returns a list of tuples of component type, name and id - """ - response = requests.get('{}/v2/sandboxes/{}/components'.format( - self._server_url, sandbox_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_component_details(self, sandbox_id, component_id): - """Get details of components in sandbox - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :return: Returns a tuple of component type, name, id, address, description - """ - response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._server_url, sandbox_id, component_id), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_component_commands(self, sandbox_id, component_id): - """Get list of commands for a particular component in sandbox - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format( - self._server_url, sandbox_id, component_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_component_command_details(self, sandbox_id, component_id, command): - """Get details of a command of sandbox component - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :param str command: Command name - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format( - self._server_url, sandbox_id, component_id, command), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def sandbox_component_command_start(self, sandbox_id, component_id, command_name, params=None): - """Start a command on sandbox component - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :param str command_name: Command name - :param dict params: parameters to be passed to the command ex: - {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} - :return: - """ - if params: - response = requests.post('{}/v2/sandboxes/{}/components/{}/commands/{}/start'.format( - self._server_url, sandbox_id, component_id, command_name), data=json.dumps(params), - headers=self._headers) - else: - response = requests.post('{}/v2/sandboxes/{}/components/{}/commands/{}/start'.format( - self._server_url, sandbox_id, component_id, command_name), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def extend_sandbox(self, sandbox_id, duration): - """Extend the sandbox - :param str sandbox_id: Sandbox id - :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) - :return: - """ - data_dict = json.loads('{"extended_time": "' + duration + '"}') - response = requests.post('{}/v2/sandboxes/{}/extend'.format(self._server_url, sandbox_id), - data=json.dumps(data_dict), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_output(self, sandbox_id): - """Get list of sandbox output - :param str sandbox_id: Sandbox id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._server_url, sandbox_id, 'output'), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def stop_sandbox(self, sandbox_id): - """Stop the sandbox given sandbox id - :param sandbox_id: Sandbox id - :return: - """ - response = requests.post('{}/v2/sandboxes/{}/stop'.format(self._server_url, sandbox_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - -usage = """ - bp_name = 'Environment1' - bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa' - sb_duration = 'PT10M' - - sb_cmd = 'blueprint_power_cycle' - sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True} - sb_component = 'Demo Resource 1' - sb_component_cmd = 'Hello' - sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} - - sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global') - r = sandbox_api.get_blueprints() - print(r) - r = sandbox_api.get_blueprint_details(bp_name) - print(r) - r = sandbox_api.get_blueprint_details(bp_id) - print(r) - r = sandbox_api.start_sandbox(bp_name, sb_duration) - print(r) - sb_id = r['id'] - r = sandbox_api.get_sandboxes() - print(r) - r = sandbox_api.get_sandbox_details(sb_id) - print(r) - r = sandbox_api.get_sandbox_commands(sb_id) - print(r) - r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup') - print(r) - r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params) - print(r) - r = sandbox_api.get_sandbox_components(sb_id) - print(r) - comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0] - - r = sandbox_api.get_sandbox_component_details(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd) - print(r) - r = sandbox_api.sandbox_component_command_start( - sb_id, comp_id, sb_component_cmd, - params=sb_comp_cmd_params) - print(r) - r = sandbox_api.extend_sandbox(sb_id, sb_duration) - print(r) - r = sandbox_api.get_sandbox_activity(sb_id) - print(r) - r = sandbox_api.get_sandbox_output(sb_id) - print(r) - print(sandbox_api.stop_sandbox(sb_id)) -""" - - -def main(): - bp_name = 'Environment1' - bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa' - sb_duration = 'PT10M' - - sb_cmd = 'blueprint_power_cycle' - sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True} - sb_component = 'Demo Resource 1' - sb_component_cmd = 'Hello' - sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} - - sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global') - r = sandbox_api.get_blueprints() - print(r) - r = sandbox_api.get_blueprint_details(bp_name) - print(r) - r = sandbox_api.get_blueprint_details(bp_id) - print(r) - r = sandbox_api.start_sandbox(bp_name, sb_duration) - print(r) - sb_id = r['id'] - r = sandbox_api.get_sandboxes() - print(r) - r = sandbox_api.get_sandbox_details(sb_id) - print(r) - r = sandbox_api.get_sandbox_commands(sb_id) - print(r) - r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup') - print(r) - r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params) - print(r) - r = sandbox_api.get_sandbox_components(sb_id) - print(r) - comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0] - - r = sandbox_api.get_sandbox_component_details(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd) - print(r) - r = sandbox_api.sandbox_component_command_start( - sb_id, comp_id, sb_component_cmd, - params=sb_comp_cmd_params) - print(r) - r = sandbox_api.extend_sandbox(sb_id, sb_duration) - print(r) - r = sandbox_api.get_sandbox_activity(sb_id) - print(r) - r = sandbox_api.get_sandbox_output(sb_id) - print(r) - print(sandbox_api.stop_sandbox(sb_id)) - - -if __name__ == '__main__': - print(usage) - main() diff --git a/Sandbox-API/quali_config.json b/Sandbox-API/quali_config.json deleted file mode 100644 index e1e47f4..0000000 --- a/Sandbox-API/quali_config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "server_name": "SERVER_NAME_OR_IP", - "server_port": "82", - "username": "USERNAME", - "password": "PASSWORD", - "domain": "DOMAIN" -} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/Sandbox-API/__init__.py b/sandbox_api/__init__.py similarity index 100% rename from Sandbox-API/__init__.py rename to sandbox_api/__init__.py diff --git a/sandbox_api/sandbox_rest.py b/sandbox_api/sandbox_rest.py new file mode 100644 index 0000000..f100dd8 --- /dev/null +++ b/sandbox_api/sandbox_rest.py @@ -0,0 +1,381 @@ +import json +from typing import List + +import requests +from dataclasses import dataclass, asdict + + +class SandboxRestException(Exception): + pass + + +@dataclass +class InputParam: + name: str + value: str + + +class SandboxRestApiClient: + """ Python wrapper for CloudShell Sandbox API """ + + def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False, + api_version="v2"): + """ logs into api and sets headers for future requests """ + protocol = "https" if is_https else "http" + self._base_url = f"{protocol}://{host}:{port}/api" + self._versioned_url = f"{self._base_url}/{api_version}" + self._session = requests.Session() + self._auth_headers = self._get_auth_headers(username, password, domain) + + def _get_auth_headers(self, user_name: str, password: str, domain: str): + """ + Get token from login response, then place token into auth headers on class + """ + login_res = requests.put(url=f"{self._base_url}/login", + data=json.dumps({"username": user_name, "password": password, "domain": domain}), + headers={"Content-Type": "application/json"}) + + if not login_res.ok: + raise SandboxRestException(self._format_err(login_res, "Failed Login")) + + login_token = login_res.text[1:-1] + + auth_headers = { + 'Authorization': f'Basic {login_token}', + 'Content-Type': 'application/json' + } + return auth_headers + + @staticmethod + def _format_err(response: requests.Response, custom_err_msg="Failed Api Call"): + err_msg = f"Response Code: {response.status_code}, Reason: {response.reason}" + if custom_err_msg: + err_msg = f"{custom_err_msg}. {err_msg}" + return err_msg + + # SANDBOX POST REQUESTS + def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", + bp_params: List[InputParam] = None, permitted_users: List[str] = None): + """ + Create a sandbox from the provided blueprint id + Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') + """ + # sandbox name will default to blueprint name if not passed + if not sandbox_name: + sandbox_name = self.get_blueprint_details(blueprint_id)['name'] + + data_dict = {"duration": duration, "name": sandbox_name} + if permitted_users: + data_dict['permitted_users'] = permitted_users + if bp_params: + data_dict["params"] = [asdict(x) for x in bp_params] + + response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start', + headers=self._auth_headers, + data=json.dumps(data_dict)) + if not response.ok: + err_msg = self._format_err(response, f"Failed to start sandbox from blueprint{blueprint_id}") + raise SandboxRestException(err_msg) + return response.json() + + def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, + permitted_users: List[str] = None): + """ Create a persistent sandbox from the provided blueprint id """ + + # sandbox name will default to blueprint name if not passed + if not sandbox_name: + sandbox_name = self.get_blueprint_details(blueprint_id)['name'] + + data_dict = {"name": sandbox_name} + if permitted_users: + data_dict['permitted_users'] = permitted_users + if bp_params: + data_dict["params"] = [asdict(x) for x in bp_params] + + response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent', + headers=self._auth_headers, + data=json.dumps(data_dict)) + if not response.ok: + err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint{blueprint_id}") + raise SandboxRestException(err_msg) + return response.json() + + def start_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, + print_output=True): + """ Run a sandbox level command """ + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start' + data_dict = {"printOutput": print_output} + params = [asdict(x) for x in params] if params else [] + data_dict["params"] = params + response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"failed to start sandbox command {command_name}")) + return response.json() + + def start_component_command(self, sandbox_id: str, component_id: str, command_name: str, + params: List[InputParam] = None, print_output: bool = True): + """ Start a command on sandbox component """ + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start' + data_dict = {"printOutput": print_output} + params = [asdict(x) for x in params] if params else [] + data_dict["params"] = params + response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"failed to start component command {command_name}")) + return response.json() + + def extend_sandbox(self, sandbox_id: str, duration: str): + """Extend the sandbox + :param str sandbox_id: Sandbox id + :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) + :return: + """ + data_dict = {"extended_time": duration} + response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend', + data=json.dumps(data_dict), + headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"failed to extend sandbox {sandbox_id}")) + return response.json() + + def stop_sandbox(self, sandbox_id: str): + """ Stop the sandbox given sandbox id """ + response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) + return response.json() + + # SANDBOX GET REQUESTS + def get_sandboxes(self): + """Get list of sandboxes + :return: + """ + response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list")) + return response.json() + + def get_sandbox_details(self, sandbox_id: str): + """ Get details of the given sandbox id """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for {sandbox_id}")) + return response.json() + + def get_sandbox_activity(self, sandbox_id: str): + """ Get list of sandbox activity """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/activity', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for {sandbox_id}")) + return response.json() + + def get_sandbox_commands(self, sandbox_id: str): + """ Get list of sandbox commands """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for {sandbox_id}")) + return response.json() + + def get_sandbox_command_details(self, sandbox_id: str, command_name: str): + """ Get details of specific sandbox command """ + response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format( + self._base_url, sandbox_id, command_name), + headers=self._headers) + if response.ok: + return json.loads(response.content) + return response.reason + + def get_sandbox_components(self, sandbox_id): + """Get list of sandbox components + :param str sandbox_id: Sandbox id + :return: Returns a list of tuples of component type, name and id + """ + response = requests.get('{}/v2/sandboxes/{}/components'.format( + self._base_url, sandbox_id), headers=self._headers) + if response.ok: + return json.loads(response.content) + return response.reason + + def get_sandbox_component_details(self, sandbox_id, component_id): + """Get details of components in sandbox + :param str sandbox_id: Sandbox id + :param str component_id: Component id + :return: Returns a tuple of component type, name, id, address, description + """ + response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._base_url, sandbox_id, component_id), + headers=self._headers) + if response.ok: + return json.loads(response.content) + return response.reason + + def get_sandbox_component_commands(self, sandbox_id, component_id): + """Get list of commands for a particular component in sandbox + :param str sandbox_id: Sandbox id + :param str component_id: Component id + :return: + """ + response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format( + self._base_url, sandbox_id, component_id), headers=self._headers) + if response.ok: + return json.loads(response.content) + return response.reason + + def get_sandbox_component_command_details(self, sandbox_id, component_id, command): + """Get details of a command of sandbox component + :param str sandbox_id: Sandbox id + :param str component_id: Component id + :param str command: Command name + :return: + """ + response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format( + self._base_url, sandbox_id, component_id, command), headers=self._headers) + if response.ok: + return json.loads(response.content) + return response.reason + + def get_sandbox_output(self, sandbox_id): + """Get list of sandbox output + :param str sandbox_id: Sandbox id + :return: + """ + response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._base_url, sandbox_id, 'output'), + headers=self._headers) + if response.ok: + return json.loads(response.content) + return response.reason + + # BLUEPRINT GET REQUESTS + def get_blueprints(self): + """ Get list of blueprints """ + response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, "Failed to get blueprints")) + return response.json() + + def get_blueprint_details(self, blueprint_id: str): + """ + Get details of a specific blueprint + Can pass either blueprint name OR blueprint ID + """ + response = requests.get(f'{self._versioned_url}/blueprints/{blueprint_id}') + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"Failed to get blueprint data for {blueprint_id}")) + return response.json() + + +usage = """ + bp_name = 'Environment1' + bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa' + sb_duration = 'PT10M' + + sb_cmd = 'blueprint_power_cycle' + sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True} + sb_component = 'Demo Resource 1' + sb_component_cmd = 'Hello' + sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} + + sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global') + r = sandbox_api.get_blueprints() + print(r) + r = sandbox_api.get_blueprint_details(bp_name) + print(r) + r = sandbox_api.get_blueprint_details(bp_id) + print(r) + r = sandbox_api.start_sandbox(bp_name, sb_duration) + print(r) + sb_id = r['id'] + r = sandbox_api.get_sandboxes() + print(r) + r = sandbox_api.get_sandbox_details(sb_id) + print(r) + r = sandbox_api.get_sandbox_commands(sb_id) + print(r) + r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup') + print(r) + r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params) + print(r) + r = sandbox_api.get_sandbox_components(sb_id) + print(r) + comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0] + + r = sandbox_api.get_sandbox_component_details(sb_id, comp_id) + print(r) + r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id) + print(r) + r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd) + print(r) + r = sandbox_api.sandbox_component_command_start( + sb_id, comp_id, sb_component_cmd, + params=sb_comp_cmd_params) + print(r) + r = sandbox_api.extend_sandbox(sb_id, sb_duration) + print(r) + r = sandbox_api.get_sandbox_activity(sb_id) + print(r) + r = sandbox_api.get_sandbox_output(sb_id) + print(r) + print(sandbox_api.stop_sandbox(sb_id)) +""" + + +def main(): + bp_name = 'Environment1' + bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa' + sb_duration = 'PT10M' + + sb_cmd = 'blueprint_power_cycle' + sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True} + sb_component = 'Demo Resource 1' + sb_component_cmd = 'Hello' + sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} + + sandbox_api = SandboxRestApiClient('localhost', 'admin', 'admin', 'Global') + r = sandbox_api.get_blueprints() + print(r) + r = sandbox_api.get_blueprint_details(bp_name) + print(r) + r = sandbox_api.get_blueprint_details(bp_id) + print(r) + r = sandbox_api.start_sandbox(bp_name, sb_duration) + print(r) + sb_id = r['id'] + r = sandbox_api.get_sandboxes() + print(r) + r = sandbox_api.get_sandbox_details(sb_id) + print(r) + r = sandbox_api.get_sandbox_commands(sb_id) + print(r) + r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup') + print(r) + r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params) + print(r) + r = sandbox_api.get_sandbox_components(sb_id) + print(r) + comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0] + + r = sandbox_api.get_sandbox_component_details(sb_id, comp_id) + print(r) + r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id) + print(r) + r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd) + print(r) + r = sandbox_api.sandbox_component_command_start( + sb_id, comp_id, sb_component_cmd, + params=sb_comp_cmd_params) + print(r) + r = sandbox_api.extend_sandbox(sb_id, sb_duration) + print(r) + r = sandbox_api.get_sandbox_activity(sb_id) + print(r) + r = sandbox_api.get_sandbox_output(sb_id) + print(r) + print(sandbox_api.stop_sandbox(sb_id)) + + +if __name__ == '__main__': + # print(usage) + # main() + api = SandboxRestApiClient("localhost", "admin", "admin") + bps = api.get_blueprints() + pass diff --git a/setup.py b/setup.py index 3740fdc..919634e 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ def readme(): classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], From be7d44930e77d80059e52902ee31188205f8ea7c Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Sat, 2 Oct 2021 15:22:13 +0300 Subject: [PATCH 03/29] refactored folder structure --- {sandbox_api => src/sandbox_api}/__init__.py | 0 .../sandbox_api}/sandbox_rest.py | 341 ++++++++---------- 2 files changed, 145 insertions(+), 196 deletions(-) rename {sandbox_api => src/sandbox_api}/__init__.py (100%) rename {sandbox_api => src/sandbox_api}/sandbox_rest.py (51%) diff --git a/sandbox_api/__init__.py b/src/sandbox_api/__init__.py similarity index 100% rename from sandbox_api/__init__.py rename to src/sandbox_api/__init__.py diff --git a/sandbox_api/sandbox_rest.py b/src/sandbox_api/sandbox_rest.py similarity index 51% rename from sandbox_api/sandbox_rest.py rename to src/sandbox_api/sandbox_rest.py index f100dd8..0ad5dd7 100644 --- a/sandbox_api/sandbox_rest.py +++ b/src/sandbox_api/sandbox_rest.py @@ -6,17 +6,22 @@ class SandboxRestException(Exception): + """ General exception to raise inside Rest client class """ pass @dataclass class InputParam: + """ To model the param objects passed to sandbox / component command endpoints """ name: str value: str class SandboxRestApiClient: - """ Python wrapper for CloudShell Sandbox API """ + """ + Python wrapper for CloudShell Sandbox API + View http:///api/v2/explore to see schemas of return json values + """ def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False, api_version="v2"): @@ -74,7 +79,7 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: - err_msg = self._format_err(response, f"Failed to start sandbox from blueprint{blueprint_id}") + err_msg = self._format_err(response, f"Failed to start sandbox from blueprint '{blueprint_id}'") raise SandboxRestException(err_msg) return response.json() @@ -96,32 +101,32 @@ def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: - err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint{blueprint_id}") + err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'") raise SandboxRestException(err_msg) return response.json() - def start_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, - print_output=True): + def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, + print_output=True): """ Run a sandbox level command """ url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start' data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers) + response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start sandbox command {command_name}")) + raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'")) return response.json() - def start_component_command(self, sandbox_id: str, component_id: str, command_name: str, - params: List[InputParam] = None, print_output: bool = True): + def run_component_command(self, sandbox_id: str, component_id: str, command_name: str, + params: List[InputParam] = None, print_output: bool = True): """ Start a command on sandbox component """ url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start' data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers) + response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start component command {command_name}")) + raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'")) return response.json() def extend_sandbox(self, sandbox_id: str, duration: str): @@ -135,7 +140,7 @@ def extend_sandbox(self, sandbox_id: str, duration: str): data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to extend sandbox {sandbox_id}")) + raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'")) return response.json() def stop_sandbox(self, sandbox_id: str): @@ -146,11 +151,12 @@ def stop_sandbox(self, sandbox_id: str): return response.json() # SANDBOX GET REQUESTS - def get_sandboxes(self): + def get_sandboxes(self, show_historic=False): """Get list of sandboxes :return: """ - response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers) + params = {"show_historic": show_historic} + response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers, params=params) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list")) return response.json() @@ -159,90 +165,129 @@ def get_sandbox_details(self, sandbox_id: str): """ Get details of the given sandbox id """ response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}', headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for {sandbox_id}")) + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'")) return response.json() - def get_sandbox_activity(self, sandbox_id: str): - """ Get list of sandbox activity """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/activity', headers=self._auth_headers) + def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, + tail: int = None): + """ + Get list of sandbox activity + 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') + 'from_event_id' - integer id of event where to start pulling results from + 'tail' - how many of the last entries you want to pull + 'error_only' - to filter for error events only + """ + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity' + params = {} + + if error_only: + params["error_only"] = error_only + if since: + params["since"] = since + if from_event_id: + params["from_event_id"] = from_event_id + if tail: + params["tail"] = tail + + if params: + response = requests.get(url, headers=self._auth_headers, params=params) + else: + response = requests.get(url, headers=self._auth_headers) + if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for {sandbox_id}")) + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'")) return response.json() def get_sandbox_commands(self, sandbox_id: str): """ Get list of sandbox commands """ response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for {sandbox_id}")) + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'")) return response.json() def get_sandbox_command_details(self, sandbox_id: str, command_name: str): """ Get details of specific sandbox command """ - response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format( - self._base_url, sandbox_id, command_name), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_components(self, sandbox_id): - """Get list of sandbox components - :param str sandbox_id: Sandbox id - :return: Returns a list of tuples of component type, name and id - """ - response = requests.get('{}/v2/sandboxes/{}/components'.format( - self._base_url, sandbox_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_component_details(self, sandbox_id, component_id): - """Get details of components in sandbox - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :return: Returns a tuple of component type, name, id, address, description - """ - response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._base_url, sandbox_id, component_id), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_component_commands(self, sandbox_id, component_id): - """Get list of commands for a particular component in sandbox - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format( - self._base_url, sandbox_id, component_id), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_component_command_details(self, sandbox_id, component_id, command): - """Get details of a command of sandbox component - :param str sandbox_id: Sandbox id - :param str component_id: Component id - :param str command: Command name - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format( - self._base_url, sandbox_id, component_id, command), headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason - - def get_sandbox_output(self, sandbox_id): - """Get list of sandbox output - :param str sandbox_id: Sandbox id - :return: - """ - response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._base_url, sandbox_id, 'output'), - headers=self._headers) - if response.ok: - return json.loads(response.content) - return response.reason + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}', + headers=self._auth_headers) + if not response.ok: + err_msg = self._format_err(response, + f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_components(self, sandbox_id: str): + """ Get list of sandbox components """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers) + if not response.ok: + err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_component_details(self, sandbox_id: str, component_id: str): + """ Get details of components in sandbox """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}', + headers=self._auth_headers) + if not response.ok: + custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', " + f"sandbox: '{sandbox_id}'") + err_msg = self._format_err(response, custom_err_msg) + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): + """ Get list of commands for a particular component in sandbox """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands', + headers=self._auth_headers) + if not response.ok: + custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', " + f"sandbox: '{sandbox_id}'") + err_msg = self._format_err(response, custom_err_msg) + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str): + """ Get details of a command of sandbox component """ + response = requests.get( + f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}', + headers=self._auth_headers) + if not response.ok: + custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', " + f"sandbox: '{sandbox_id}'") + err_msg = self._format_err(response, custom_err_msg) + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_instructions(self, sandbox_id: str): + """ pull the instruction text of sandbox """ + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions', + headers=self._auth_headers) + if not response.ok: + err_msg = self._format_err(response, + f"Failed to get sandbox instructions for '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None): + """ Get list of sandbox output """ + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions' + params = {} + if tail: + params["tail"] = tail + if from_entry_id: + params["from_entry_id"] = from_entry_id + if since: + params["since"] = since + + if params: + response = requests.get(url, headers=self._auth_headers, params=params) + else: + response = requests.get(url, headers=self._auth_headers) + + if not response.ok: + err_msg = self._format_err(response, + f"Failed to get sandbox instructions for '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() # BLUEPRINT GET REQUESTS def get_blueprints(self): @@ -257,125 +302,29 @@ def get_blueprint_details(self, blueprint_id: str): Get details of a specific blueprint Can pass either blueprint name OR blueprint ID """ - response = requests.get(f'{self._versioned_url}/blueprints/{blueprint_id}') + response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'") if not response.ok: raise SandboxRestException( - self._format_err(response, f"Failed to get blueprint data for {blueprint_id}")) + self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) return response.json() + # EXECUTIONS + def get_execution_details(self, execution_id: str): + response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"Failed to get execution details for '{execution_id}'")) + return response.json() -usage = """ - bp_name = 'Environment1' - bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa' - sb_duration = 'PT10M' - - sb_cmd = 'blueprint_power_cycle' - sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True} - sb_component = 'Demo Resource 1' - sb_component_cmd = 'Hello' - sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} - - sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global') - r = sandbox_api.get_blueprints() - print(r) - r = sandbox_api.get_blueprint_details(bp_name) - print(r) - r = sandbox_api.get_blueprint_details(bp_id) - print(r) - r = sandbox_api.start_sandbox(bp_name, sb_duration) - print(r) - sb_id = r['id'] - r = sandbox_api.get_sandboxes() - print(r) - r = sandbox_api.get_sandbox_details(sb_id) - print(r) - r = sandbox_api.get_sandbox_commands(sb_id) - print(r) - r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup') - print(r) - r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params) - print(r) - r = sandbox_api.get_sandbox_components(sb_id) - print(r) - comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0] - - r = sandbox_api.get_sandbox_component_details(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd) - print(r) - r = sandbox_api.sandbox_component_command_start( - sb_id, comp_id, sb_component_cmd, - params=sb_comp_cmd_params) - print(r) - r = sandbox_api.extend_sandbox(sb_id, sb_duration) - print(r) - r = sandbox_api.get_sandbox_activity(sb_id) - print(r) - r = sandbox_api.get_sandbox_output(sb_id) - print(r) - print(sandbox_api.stop_sandbox(sb_id)) -""" - - -def main(): - bp_name = 'Environment1' - bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa' - sb_duration = 'PT10M' - - sb_cmd = 'blueprint_power_cycle' - sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True} - sb_component = 'Demo Resource 1' - sb_component_cmd = 'Hello' - sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True} - - sandbox_api = SandboxRestApiClient('localhost', 'admin', 'admin', 'Global') - r = sandbox_api.get_blueprints() - print(r) - r = sandbox_api.get_blueprint_details(bp_name) - print(r) - r = sandbox_api.get_blueprint_details(bp_id) - print(r) - r = sandbox_api.start_sandbox(bp_name, sb_duration) - print(r) - sb_id = r['id'] - r = sandbox_api.get_sandboxes() - print(r) - r = sandbox_api.get_sandbox_details(sb_id) - print(r) - r = sandbox_api.get_sandbox_commands(sb_id) - print(r) - r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup') - print(r) - r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params) - print(r) - r = sandbox_api.get_sandbox_components(sb_id) - print(r) - comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0] - - r = sandbox_api.get_sandbox_component_details(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id) - print(r) - r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd) - print(r) - r = sandbox_api.sandbox_component_command_start( - sb_id, comp_id, sb_component_cmd, - params=sb_comp_cmd_params) - print(r) - r = sandbox_api.extend_sandbox(sb_id, sb_duration) - print(r) - r = sandbox_api.get_sandbox_activity(sb_id) - print(r) - r = sandbox_api.get_sandbox_output(sb_id) - print(r) - print(sandbox_api.stop_sandbox(sb_id)) + def delete_execution(self, execution_id: str): + response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"Failed to delete execution for '{execution_id}'")) + return response.json() if __name__ == '__main__': - # print(usage) - # main() api = SandboxRestApiClient("localhost", "admin", "admin") bps = api.get_blueprints() pass From 23475df86bc5be748c8bfc921a503190332ce3c6 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Wed, 6 Oct 2021 00:12:59 +0100 Subject: [PATCH 04/29] added auth methods --- .gitignore | 4 +- build/lib/cloudshell/__init__.py | 3 + build/lib/cloudshell/sandbox_rest/__init__.py | 3 + .../cloudshell/sandbox_rest/flow_helpers.py | 0 .../cloudshell/sandbox_rest/sandbox_api.py | 50 +-- cloudshell/__init__.py | 3 + cloudshell/sandbox_rest/__init__.py | 3 + .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 283 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 287 bytes .../__pycache__/exceptions.cpython-37.pyc | Bin 0 -> 802 bytes .../__pycache__/sandbox_api.cpython-39.pyc | Bin 0 -> 13066 bytes cloudshell/sandbox_rest/exceptions.py | 13 + cloudshell/sandbox_rest/flow_helpers.py | 0 cloudshell/sandbox_rest/sandbox_api.py | 419 ++++++++++++++++++ .../PKG-INFO | 41 ++ .../SOURCES.txt | 11 + .../dependency_links.txt | 1 + .../requires.txt | 1 + .../top_level.txt | 1 + ...cloudshell_sandboxapi_wrapper-2.0.0.tar.gz | Bin 0 -> 5103 bytes setup.py | 35 +- test-requirements.txt | 1 + tests/test_rest_client.py | 51 +++ version.txt | 1 + 24 files changed, 599 insertions(+), 42 deletions(-) create mode 100644 build/lib/cloudshell/__init__.py create mode 100644 build/lib/cloudshell/sandbox_rest/__init__.py rename src/sandbox_api/__init__.py => build/lib/cloudshell/sandbox_rest/flow_helpers.py (100%) rename src/sandbox_api/sandbox_rest.py => build/lib/cloudshell/sandbox_rest/sandbox_api.py (90%) create mode 100644 cloudshell/__init__.py create mode 100644 cloudshell/sandbox_rest/__init__.py create mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc create mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc create mode 100644 cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc create mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc create mode 100644 cloudshell/sandbox_rest/exceptions.py create mode 100644 cloudshell/sandbox_rest/flow_helpers.py create mode 100644 cloudshell/sandbox_rest/sandbox_api.py create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/requires.txt create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/top_level.txt create mode 100644 dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz create mode 100644 test-requirements.txt create mode 100644 tests/test_rest_client.py create mode 100644 version.txt diff --git a/.gitignore b/.gitignore index 62c8935..8750fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.idea/ \ No newline at end of file +.idea/ +.pytest_cache/ +*.egg \ No newline at end of file diff --git a/build/lib/cloudshell/__init__.py b/build/lib/cloudshell/__init__.py new file mode 100644 index 0000000..b50dc4a --- /dev/null +++ b/build/lib/cloudshell/__init__.py @@ -0,0 +1,3 @@ +__author__ = 'quali' +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) \ No newline at end of file diff --git a/build/lib/cloudshell/sandbox_rest/__init__.py b/build/lib/cloudshell/sandbox_rest/__init__.py new file mode 100644 index 0000000..b50dc4a --- /dev/null +++ b/build/lib/cloudshell/sandbox_rest/__init__.py @@ -0,0 +1,3 @@ +__author__ = 'quali' +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) \ No newline at end of file diff --git a/src/sandbox_api/__init__.py b/build/lib/cloudshell/sandbox_rest/flow_helpers.py similarity index 100% rename from src/sandbox_api/__init__.py rename to build/lib/cloudshell/sandbox_rest/flow_helpers.py diff --git a/src/sandbox_api/sandbox_rest.py b/build/lib/cloudshell/sandbox_rest/sandbox_api.py similarity index 90% rename from src/sandbox_api/sandbox_rest.py rename to build/lib/cloudshell/sandbox_rest/sandbox_api.py index 0ad5dd7..ecc62ef 100644 --- a/src/sandbox_api/sandbox_rest.py +++ b/build/lib/cloudshell/sandbox_rest/sandbox_api.py @@ -25,11 +25,10 @@ class SandboxRestApiClient: def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False, api_version="v2"): - """ logs into api and sets headers for future requests """ + """ login to api and store headers for future requests """ protocol = "https" if is_https else "http" self._base_url = f"{protocol}://{host}:{port}/api" self._versioned_url = f"{self._base_url}/{api_version}" - self._session = requests.Session() self._auth_headers = self._get_auth_headers(username, password, domain) def _get_auth_headers(self, user_name: str, password: str, domain: str): @@ -65,19 +64,17 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", Create a sandbox from the provided blueprint id Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') """ - # sandbox name will default to blueprint name if not passed - if not sandbox_name: - sandbox_name = self.get_blueprint_details(blueprint_id)['name'] - - data_dict = {"duration": duration, "name": sandbox_name} - if permitted_users: - data_dict['permitted_users'] = permitted_users - if bp_params: - data_dict["params"] = [asdict(x) for x in bp_params] - - response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start', - headers=self._auth_headers, - data=json.dumps(data_dict)) + url = f'{self._versioned_url}/blueprints/{blueprint_id}/start' + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + + data_dict = { + "duration": duration, + "name": sandbox_name, + "permitted_users": permitted_users if permitted_users else [], + "params": [asdict(x) for x in bp_params] if bp_params else [] + } + + response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: err_msg = self._format_err(response, f"Failed to start sandbox from blueprint '{blueprint_id}'") raise SandboxRestException(err_msg) @@ -86,20 +83,16 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None): """ Create a persistent sandbox from the provided blueprint id """ + url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent' - # sandbox name will default to blueprint name if not passed - if not sandbox_name: - sandbox_name = self.get_blueprint_details(blueprint_id)['name'] - - data_dict = {"name": sandbox_name} - if permitted_users: - data_dict['permitted_users'] = permitted_users - if bp_params: - data_dict["params"] = [asdict(x) for x in bp_params] + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + data_dict = { + "name": sandbox_name, + "permitted_users": permitted_users if permitted_users else [], + "params": [asdict(x) for x in bp_params] if bp_params else [] + } - response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent', - headers=self._auth_headers, - data=json.dumps(data_dict)) + response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'") raise SandboxRestException(err_msg) @@ -126,7 +119,8 @@ def run_component_command(self, sandbox_id: str, component_id: str, command_name data_dict["params"] = params response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'")) + raise SandboxRestException( + self._format_err(response, f"failed to start component command '{command_name}'")) return response.json() def extend_sandbox(self, sandbox_id: str, duration: str): diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py new file mode 100644 index 0000000..b50dc4a --- /dev/null +++ b/cloudshell/__init__.py @@ -0,0 +1,3 @@ +__author__ = 'quali' +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) \ No newline at end of file diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py new file mode 100644 index 0000000..b50dc4a --- /dev/null +++ b/cloudshell/sandbox_rest/__init__.py @@ -0,0 +1,3 @@ +__author__ = 'quali' +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) \ No newline at end of file diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16c0d26a19c161f66f05ab525034462bea0f8f07 GIT binary patch literal 283 zcmXv{%TB{E5VVt2M1}qW;+jMI0Tn_#PFzr>iUYyY;>Igt>e#_{r1s2(U*VU0<-{*= z>Nbdxc6N4Wq`95Vri}3U{3Ipq@8j{0oWwOvI44r9W{RuK@Gh$}&M?Cqi;t|%Rla6b zktXkhH2R04tm5eucM+|EUdHyJnxrX!9AfJ{KsxESuS3+v9}7|mV~GBwtn3iri}{kQ zk$zw9mf|s>55meA^EeJU?Irv`&poJvDMRPZx?0L>m!5C8xG literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25178204b2e5b5ce4876c516480741b828ffc7da GIT binary patch literal 287 zcmYjKF;2uV5VW0~hz{-xh&ILD0}w(WDjIasiGpBhG4YC+bL?O{(p^o%SNM`!D!xF) zUT7F;XLoi+8nsx=8R6scK`Gi_2k{>Ti7T3TN~BrCG}prNUNnL;6j)&SE}B9Y8&;Qj z_BJYOzA4ITo{w>t&}kS{>h9`Uo&%^cbv^*(qhb3pCS${)AXhMj=uekZJpz2PP?0s! z&(qDhypI^7bSfpY+RD~zlvA7RO>EDG;9s##QErse&wjVOUf(X)dunsCwSLsGLu+OH fb0J_%62Lf<09M03R~Nn4qs2>3rLiO``3e68{Qpuf literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..178f54132054515b69782940d859da31f7c48a9c GIT binary patch literal 802 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 literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py new file mode 100644 index 0000000..4b3a7ac --- /dev/null +++ b/cloudshell/sandbox_rest/exceptions.py @@ -0,0 +1,13 @@ +class SandboxRestException(Exception): + """ General exception to raise inside Rest client class """ + pass + + +class SandboxRestAuthException(Exception): + """ Failed auth actions """ + pass + + +class SandboxRestInitException(ValueError): + """ Failed auth actions """ + pass diff --git a/cloudshell/sandbox_rest/flow_helpers.py b/cloudshell/sandbox_rest/flow_helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py new file mode 100644 index 0000000..3c99893 --- /dev/null +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -0,0 +1,419 @@ +import json +import time +from typing import List +import requests +from dataclasses import dataclass, asdict +from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException + + +@dataclass +class InputParam: + """ + param objects passed to sandbox / component command endpoints + sandbox global inputs, commands and resource commands all follow this generic name/value convention + """ + name: str + value: str + + +class SandboxRestApiClient: + """ + Python wrapper for CloudShell Sandbox API + View http:///api/v2/explore to see schemas of return json values + """ + + def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False, + api_version="v2"): + """ login to api and store headers for future requests """ + _protocol = "https" if is_https else "http" + self.host = host + self.username = username + self._password = password + self.domain = domain + self._base_url = f"{_protocol}://{host}:{port}/api" + self._versioned_url = f"{self._base_url}/{api_version}" + self.auth_token = self.get_token_with_credentials(username, password, domain) + self._auth_headers = self._build_auth_headers(self.auth_token) + + # CONTEXT MANAGER METHODS + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + # invalidate stored api token on exit + self.invalidate_auth() + + @staticmethod + def _build_auth_headers(login_token: str) -> dict: + """ + interpolate token into auth_headers dict + """ + auth_headers = { + 'Authorization': f'Basic {login_token}', + 'Content-Type': 'application/json' + } + return auth_headers + + def refresh_auth(self): + self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) + self._auth_headers = self._build_auth_headers(self.auth_token) + + def invalidate_auth(self): + self.invalidate_target_token(self.auth_token) + self._auth_headers = None + + def _validate_auth_headers(self): + if not self._auth_headers: + raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session") + + # LOGIN METHODS + def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str: + """ + Get token from credentials - extraneous quotes stripped off token string + """ + login_res = requests.put(url=f"{self._base_url}/login", + data=json.dumps({"username": user_name, "password": password, "domain": domain}), + headers={"Content-Type": "application/json"}) + + if not login_res.ok: + raise SandboxRestAuthException(self._format_err(login_res, "Failed Login")) + + login_token = login_res.text[1:-1] + return login_token + + def get_token_for_target_user(self, user_name: str) -> str: + """ + Get token for target user - remove extraneous quotes + """ + self._validate_auth_headers() + login_res = requests.post(url=f"{self._base_url}/token", + data=json.dumps({"username": user_name}), + headers=self._auth_headers) + + if not login_res.ok: + raise SandboxRestException(self._format_err(login_res, f"Failed to get get token for user {user_name}")) + + login_token = login_res.text[1:-1] + return login_token + + def invalidate_target_token(self, token_id: str) -> None: + self._validate_auth_headers() + login_res = requests.delete(url=f"{self._base_url}/token/{token_id}", + headers=self._auth_headers) + if not login_res.ok: + raise SandboxRestException(self._format_err(login_res, "Failed to delete token")) + + def _format_err(self, response: requests.Response, custom_err_msg="Failed Api Call"): + response_data = (f"Response: {response.status_code}, Reason: {response.reason}\n" + f"Request URL: {response.request.url}\n" + f"Request Headers: {response.request.headers}") + return (f"Sandbox API Error for User: '{self.username}', Domain: '{self.domain}'.\n" + f"{custom_err_msg}\n" + f"{response_data}") + + # SANDBOX POST REQUESTS + def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", + bp_params: List[InputParam] = None, permitted_users: List[str] = None): + """ + Create a sandbox from the provided blueprint id + Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') + """ + self._validate_auth_headers() + url = f'{self._versioned_url}/blueprints/{blueprint_id}/start' + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + + data_dict = { + "duration": duration, + "name": sandbox_name, + "permitted_users": permitted_users if permitted_users else [], + "params": [asdict(x) for x in bp_params] if bp_params else [] + } + + response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) + if not response.ok: + custom_msg = f"Failed to start '{blueprint_id}'. " + if 400 <= response.status_code < 500: + custom_msg += "Ensure blueprint is 'Public' and in correct domain." + if response.status_code >= 500: + custom_msg += "Internal Server 500 Error During Request." + err_msg = self._format_err(response, custom_msg) + raise SandboxRestException(err_msg) + return response.json() + + def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, + permitted_users: List[str] = None): + """ Create a persistent sandbox from the provided blueprint id """ + self._validate_auth_headers() + url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent' + + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + data_dict = { + "name": sandbox_name, + "permitted_users": permitted_users if permitted_users else [], + "params": [asdict(x) for x in bp_params] if bp_params else [] + } + + response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) + if not response.ok: + err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, + print_output=True): + """ Run a sandbox level command """ + self._validate_auth_headers() + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start' + data_dict = {"printOutput": print_output} + params = [asdict(x) for x in params] if params else [] + data_dict["params"] = params + response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'")) + return response.json() + + def run_component_command(self, sandbox_id: str, component_id: str, command_name: str, + params: List[InputParam] = None, print_output: bool = True): + """ Start a command on sandbox component """ + self._validate_auth_headers() + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start' + data_dict = {"printOutput": print_output} + params = [asdict(x) for x in params] if params else [] + data_dict["params"] = params + response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"failed to start component command '{command_name}'")) + return response.json() + + def extend_sandbox(self, sandbox_id: str, duration: str): + """Extend the sandbox + :param str sandbox_id: Sandbox id + :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) + :return: + """ + self._validate_auth_headers() + data_dict = {"extended_time": duration} + response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend', + data=json.dumps(data_dict), + headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'")) + return response.json() + + def stop_sandbox(self, sandbox_id: str): + """ Stop the sandbox given sandbox id """ + self._validate_auth_headers() + response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) + return response.json() + + # SANDBOX GET REQUESTS + def get_sandboxes(self, show_historic=False): + """ Get list of sandboxes """ + self._validate_auth_headers() + url = f'{self._versioned_url}/sandboxes' + params = {"show_historic": "true" if show_historic else "false"} + response = requests.get(url, headers=self._auth_headers, params=params) + if not response.ok: + err_msg = self._format_err(response, f"Failed to get sandbox list") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_details(self, sandbox_id: str): + """ Get details of the given sandbox id """ + self._validate_auth_headers() + url = f'{self._versioned_url}/sandboxes/{sandbox_id}' + response = requests.get(url, headers=self._auth_headers) + if not response.ok: + exc_msg = self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'") + raise SandboxRestException(exc_msg) + return response.json() + + def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, + tail: int = None): + """ + Get list of sandbox activity + 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') + 'from_event_id' - integer id of event where to start pulling results from + 'tail' - how many of the last entries you want to pull + 'error_only' - to filter for error events only + """ + self._validate_auth_headers() + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity' + params = {} + + if error_only: + params["error_only"] = error_only + if since: + params["since"] = since + if from_event_id: + params["from_event_id"] = from_event_id + if tail: + params["tail"] = tail + + if params: + response = requests.get(url, headers=self._auth_headers, params=params) + else: + response = requests.get(url, headers=self._auth_headers) + + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'")) + return response.json() + + def get_sandbox_commands(self, sandbox_id: str): + """ Get list of sandbox commands """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'")) + return response.json() + + def get_sandbox_command_details(self, sandbox_id: str, command_name: str): + """ Get details of specific sandbox command """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}', + headers=self._auth_headers) + if not response.ok: + err_msg = self._format_err(response, + f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_components(self, sandbox_id: str): + """ Get list of sandbox components """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers) + if not response.ok: + err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_component_details(self, sandbox_id: str, component_id: str): + """ Get details of components in sandbox """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}', + headers=self._auth_headers) + if not response.ok: + custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', " + f"sandbox: '{sandbox_id}'") + err_msg = self._format_err(response, custom_err_msg) + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): + """ Get list of commands for a particular component in sandbox """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands', + headers=self._auth_headers) + if not response.ok: + custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', " + f"sandbox: '{sandbox_id}'") + err_msg = self._format_err(response, custom_err_msg) + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str): + """ Get details of a command of sandbox component """ + self._validate_auth_headers() + response = requests.get( + f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}', + headers=self._auth_headers) + if not response.ok: + custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', " + f"sandbox: '{sandbox_id}'") + err_msg = self._format_err(response, custom_err_msg) + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_instructions(self, sandbox_id: str): + """ pull the instruction text of sandbox """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions', + headers=self._auth_headers) + if not response.ok: + err_msg = self._format_err(response, + f"Failed to get sandbox instructions for '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None): + """ Get list of sandbox output """ + self._validate_auth_headers() + url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions' + params = {} + if tail: + params["tail"] = tail + if from_entry_id: + params["from_entry_id"] = from_entry_id + if since: + params["since"] = since + + if params: + response = requests.get(url, headers=self._auth_headers, params=params) + else: + response = requests.get(url, headers=self._auth_headers) + + if not response.ok: + err_msg = self._format_err(response, + f"Failed to get sandbox instructions for '{sandbox_id}'") + raise SandboxRestException(err_msg) + return response.json() + + # BLUEPRINT GET REQUESTS + def get_blueprints(self): + """ Get list of blueprints """ + self._validate_auth_headers() + response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers) + if not response.ok: + raise SandboxRestException(self._format_err(response, "Failed to get blueprints")) + return response.json() + + def get_blueprint_details(self, blueprint_id: str): + """ + Get details of a specific blueprint + Can pass either blueprint name OR blueprint ID + """ + self._validate_auth_headers() + response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'", headers=self._auth_headers) + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) + return response.json() + + # EXECUTIONS + def get_execution_details(self, execution_id: str): + self._validate_auth_headers() + response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"Failed to get execution details for '{execution_id}'")) + return response.json() + + def delete_execution(self, execution_id: str): + self._validate_auth_headers() + response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) + if not response.ok: + raise SandboxRestException( + self._format_err(response, f"Failed to delete execution for '{execution_id}'")) + return response.json() + + +if __name__ == '__main__': + API_SERVER = "localhost" + ADMIN_USER = "admin" + ADMIN_PASS = "admin" + + admin_api = SandboxRestApiClient(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS) + with admin_api: + # sample api call with admin user session to get all sandboxes + all_sandboxes = admin_api.get_sandboxes() + print("== List of sandboxes pulled by Admin ===") + print(json.dumps(all_sandboxes, indent=4)) + + admin_api.refresh_auth() + all_sandboxes = admin_api.get_sandboxes() + pass + + diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO new file mode 100644 index 0000000..df7ebb0 --- /dev/null +++ b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO @@ -0,0 +1,41 @@ +Metadata-Version: 1.1 +Name: cloudshell-sandboxapi-wrapper +Version: 2.0.0 +Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI +Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python +Author: sadanand.s +Author-email: sadanand.s@quali.com +License: Apache 2.0 +Description: CloudShell Sandbox API Wrapper + ============================== + + Installation + ************* + :: + + pip install cloudshell_sandboxapi_wrapper + + Example Usage + ************** + :: + + from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI + sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT) + blueprints = sandbox.get_blueprints() + blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id'] + sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME) + sandbox.stop_sandbox(sandbox_id) + + | + + :Note: + Tested on cloudshell 9.3 with Python 2.7/3.7/3.8. + For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API `_ + + | +Keywords: cloudshell,sandbox,api,CI +Platform: UNKNOWN +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Natural Language :: English +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt new file mode 100644 index 0000000..e9351d7 --- /dev/null +++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt @@ -0,0 +1,11 @@ +README.rst +setup.py +cloudshell/__init__.py +cloudshell/sandbox_rest/__init__.py +cloudshell/sandbox_rest/flow_helpers.py +cloudshell/sandbox_rest/sandbox_api.py +cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO +cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt +cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt +cloudshell_sandboxapi_wrapper.egg-info/requires.txt +cloudshell_sandboxapi_wrapper.egg-info/top_level.txt \ No newline at end of file diff --git a/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt b/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt @@ -0,0 +1 @@ +requests diff --git a/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt b/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt new file mode 100644 index 0000000..127ff4f --- /dev/null +++ b/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt @@ -0,0 +1 @@ +cloudshell diff --git a/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz b/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6424e232d889ab652ddd54d37f98245b742d9a01 GIT binary patch literal 5103 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 str: + with open(file_name) as f: + content = f.read().strip() + return content -def readme(): - with open('README.rst') as f: - return f.read() + +def lines_from_file(file_name: str) -> List[str]: + with open(file_name) as f: + lines = f.read().splitlines() + return lines setup( name='cloudshell_sandboxapi_wrapper', - version='1.0.0', - packages=['cloudshell_sandboxapi_wrapper'], - url='http://www.quali.com', - license='Apache 2.0', + description='Python client for CloudShell Sandbox REST api - consume sandboxes via CI', + keywords=["cloudshell", "sandbox", "api", "CI"], + url='https://github.com/QualiSystemsLab/Sandbox-API-Python', author='sadanand.s', author_email='sadanand.s@quali.com', - description='Python wrapper for CloudShell Sandbox API', - long_description=readme(), + license='Apache 2.0', + packages=find_packages(), + version=read_file("version.txt"), + long_description=read_file("README.rst"), + install_requires=lines_from_file("requirements.txt"), + test_requires=lines_from_file("test-requirements.txt"), classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', - ], - install_requires=['requests'] -) + ] +) \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test_rest_client.py b/tests/test_rest_client.py new file mode 100644 index 0000000..0e5af88 --- /dev/null +++ b/tests/test_rest_client.py @@ -0,0 +1,51 @@ +import pytest +from cloudshell_test.sandbox_rest.sandbox_api import SandboxRestApiClient +from datetime import datetime +import json +from dataclasses import dataclass + +DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" +DUT_RESOURCE = "DUT_1" +HEALTH_CHECK_COMMAND = "health_check" + + +@dataclass +class SandboxTestData: + blueprint_name: str + sandbox_command: str + + +@dataclass +class ResourceTestData: + resource_name: str + resource_command: str + + +@pytest.fixture(name="dut_bp") +def dut_blueprint_fixture(): + """ For testing basic operations """ + return "DUT Test" + + +@pytest.fixture(name="session") +def rest_session_fixture(): + return SandboxRestApiClient("localhost", "admin", "admin") + + +def _pretty_print_response(dict_response): + json_str = json.dumps(dict_response, indent=4) + print(f"\n{json_str}") + + +class TestNoBlueprint: + def test_get_sandboxes(self, session: SandboxRestApiClient): + res = session.get_sandboxes() + _pretty_print_response(res) + + +class TestEmptyBlueprint: + bp_data = SandboxTestData(blueprint_name=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_command="Setup") + + def test_simple_flow(self, session: SandboxRestApiClient): + start_response = session.start_sandbox(self.bp_data.blueprint_name, f"test sandbox - {datetime.now()}") + print(f"\nblueprint {start_response['name']} started") diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..359a5b9 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2.0.0 \ No newline at end of file From 25d1987b6b3d46a0898e70524c9a0c7cc285b437 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Fri, 8 Oct 2021 23:42:00 +0100 Subject: [PATCH 05/29] components class added --- .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 270 bytes .../__pycache__/exceptions.cpython-37.pyc | Bin 802 -> 0 bytes .../__pycache__/sandbox_api.cpython-37.pyc | Bin 0 -> 16768 bytes cloudshell/sandbox_rest/exceptions.py | 13 --- .../{flow_helpers.py => helpers/__init__.py} | 0 .../helpers/concurrency_helper.py | 0 .../sandbox_rest/helpers/polling_helpers.py | 96 +++++++++++++++++ cloudshell/sandbox_rest/sandbox_api.py | 35 +++++-- cloudshell/sandbox_rest/sandbox_components.py | 98 ++++++++++++++++++ cloudshell/sandbox_rest/sandbox_controller.py | 89 ++++++++++++++++ requirements.txt | 3 +- 11 files changed, 310 insertions(+), 24 deletions(-) create mode 100644 cloudshell/__pycache__/__init__.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc create mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/exceptions.py rename cloudshell/sandbox_rest/{flow_helpers.py => helpers/__init__.py} (100%) create mode 100644 cloudshell/sandbox_rest/helpers/concurrency_helper.py create mode 100644 cloudshell/sandbox_rest/helpers/polling_helpers.py create mode 100644 cloudshell/sandbox_rest/sandbox_components.py create mode 100644 cloudshell/sandbox_rest/sandbox_controller.py diff --git a/cloudshell/__pycache__/__init__.cpython-37.pyc b/cloudshell/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1638dc996911c33a01b65615684ced948aabc95d GIT binary patch literal 270 zcmXwyO-chn5QV#YCK{r-g1DVUa)2P>&&rJi1~)@X(M}gmo1X5me;`?V8m}~47vdFM zSsCy_ef5fZ@a~q&1tV>~pOvEhbv*u6kh!6W7X-~3rnwfD_o5M;p}+#m57895s99Au zv-hm5`JpJQ_;`lz!=9Mme>~-o*BD2>uP*6lJZPe)0R&?e<}{Jy4sIt@T;Q4y}~{ Z#+d}L9uDLDs`oluyynysOG3%d`5y=LOV|Ja literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc deleted file mode 100644 index 178f54132054515b69782940d859da31f7c48a9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 802 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 bool: + """ if still in setup keep polling """ + setup_states = [SandboxStates.before_setup_state.value, SandboxStates.running_setup_state.value] + if sandbox_details["state"] in setup_states: + return True + return False + + +def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool: + """ if in teardown keep polling """ + if sandbox_details["state"] == SandboxStates.teardown_state.value: + return True + return False + + +def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_func: Callable, + max_polling_minutes: int, polling_frequency_seconds: int) -> str: + """ Create blocking polling process """ + + # retry wait times are in milliseconds + @retry(retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000, + stop_max_delay=max_polling_minutes * 60000) + def get_sandbox_details(): + return api.get_sandbox_details(reservation_id) + + return get_sandbox_details() + + +def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20, + polling_frequency_seconds=30) -> dict: + """ poll until completion """ + sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, + polling_frequency_seconds) + return sandbox_details + + +def poll_sandbox_teardown(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20, + polling_frequency_seconds=30) -> dict: + """ poll until completion """ + sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes, + polling_frequency_seconds) + return sandbox_details + + +def _should_we_keep_polling_execution(exc_data: dict) -> bool: + current_exc_status = exc_data["status"] + if current_exc_status in UNFINISHED_EXECUTION_STATUSES: + return True + return False + + +def poll_execution_for_completion(sandbox_rest: SandboxRestApiClient, command_execution_id: str, + max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str: + """ + poll execution for "Completed" status, then return the execution output + """ + # retry wait times are in milliseconds + @retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000, + stop_max_delay=max_polling_in_minutes * 60000) + def get_execution_data(): + exc_data = sandbox_rest.get_execution_details(command_execution_id) + return exc_data + return get_execution_data(sandbox_rest, command_execution_id) + + diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 3c99893..7b6803b 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -3,7 +3,21 @@ from typing import List import requests from dataclasses import dataclass, asdict -from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException + + +class SandboxRestException(Exception): + """ General exception to raise inside Rest client class """ + pass + + +class SandboxRestAuthException(Exception): + """ Failed auth actions """ + pass + + +class SandboxRestInitException(ValueError): + """ Failed auth actions """ + pass @dataclass @@ -16,7 +30,7 @@ class InputParam: value: str -class SandboxRestApiClient: +class SandboxRestApiSession: """ Python wrapper for CloudShell Sandbox API View http:///api/v2/explore to see schemas of return json values @@ -207,7 +221,6 @@ def stop_sandbox(self, sandbox_id: str): response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) - return response.json() # SANDBOX GET REQUESTS def get_sandboxes(self, show_historic=False): @@ -405,15 +418,17 @@ def delete_execution(self, execution_id: str): ADMIN_USER = "admin" ADMIN_PASS = "admin" - admin_api = SandboxRestApiClient(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS) + admin_api = SandboxRestApiSession(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS) with admin_api: # sample api call with admin user session to get all sandboxes - all_sandboxes = admin_api.get_sandboxes() - print("== List of sandboxes pulled by Admin ===") - print(json.dumps(all_sandboxes, indent=4)) + # all_sandboxes = admin_api.get_sandboxes() + # print("== List of sandboxes pulled by Admin ===") + # print(json.dumps(all_sandboxes, indent=4)) + + sb_res = admin_api.start_sandbox(blueprint_id="rest test", sandbox_name="rest test") + print(sb_res["state"]) + time.sleep(4) + - admin_api.refresh_auth() - all_sandboxes = admin_api.get_sandboxes() - pass diff --git a/cloudshell/sandbox_rest/sandbox_components.py b/cloudshell/sandbox_rest/sandbox_components.py new file mode 100644 index 0000000..6fd9208 --- /dev/null +++ b/cloudshell/sandbox_rest/sandbox_components.py @@ -0,0 +1,98 @@ +import json +from typing import List +from enum import Enum + + +class ComponentTypes(Enum): + app_type = "Application" + resource_type = "Resource" + service_type = "Service" + + +class AppLifeCycleTypes(Enum): + deployed = "Deployed" + un_deployed = "Undeployed" + + +class AttributeTypes(Enum): + boolean_type = "boolean" + password_type = "password" + string_type = "string" + numeric_type = "numeric" + + +class SandboxRestComponents: + def __init__(self, components: List[dict] = None): + """ + if instantiated with components then populate lists + if not, then call "refresh_components" after getting data from sandbox + """ + self.all_components = components if components else [] + self.services = [] + self.resources = [] + self.deployed_apps = [] + self.un_deployed_apps = [] + if self.all_components: + self._sort_components() + + def _filter_components_by_type(self, component_type: str) -> List[dict]: + """ accepts both short info components and full info """ + return [component for component in self.all_components if component["type"] == component_type] + + def _filter_app_by_lifecycle(self, lifecycle_type): + return [component for component in self.all_components + if component["type"] == ComponentTypes.app_type.value + and component["app_lifecyle"] == lifecycle_type] + + def _sort_components(self) -> None: + """ sort stored components into separate lists """ + self.resources = self._filter_components_by_type(ComponentTypes.resource_type.value) + self.services = self._filter_components_by_type(ComponentTypes.service_type.value) + self.deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.deployed.value) + self.un_deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.un_deployed.value) + + def refresh_components(self, components: List[dict]) -> None: + self.all_components = components + self._sort_components() + + def find_component_by_name(self, component_name: str) -> dict: + for component in self.all_components: + if component["name"] == component_name: + return component + + @staticmethod + def filter_by_model(components: List[dict], model: str) -> List[dict]: + """ + Can pass in all components or sub list + Both Resources and Applications can use same shell / model + """ + return [component for component in components if component["component_type"] == model] + + def filter_by_attr_value(self, components: List[dict], attribute_name: str, attribute_value: str) -> List[dict]: + """ attribute name does not have to include model / family namespace """ + self._validate_components_for_attributes(components) + result = [] + for component in components: + for attr in component["attributes"]: + if attr["name"].endswith(attribute_name) and attr["value"] == attribute_value: + result.append(component) + return result + + def filter_by_boolean_attr_true(self, components: List[dict], attribute_name: str) -> List[dict]: + """ attribute name does not have to include model / family namespace """ + self._validate_components_for_attributes(components) + result = [] + for component in components: + for attr in component["attributes"]: + if attr["name"].endswith(attribute_name) and attr["value"].lower() == "true": + result.append(component) + return result + + @staticmethod + def _validate_components_for_attributes(components: List[dict]): + if not components: + return + attrs = components[0].get("attributes") + if not attrs: + raise Exception("'attributes' member not found. Must pass in Full info components.\n" + f"components data passed:\n{json.dumps(components, indent=4)}") diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py new file mode 100644 index 0000000..1e52f33 --- /dev/null +++ b/cloudshell/sandbox_rest/sandbox_controller.py @@ -0,0 +1,89 @@ +import json +import logging +from typing import List + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam +from cloudshell.sandbox_rest.helpers import polling_helpers as poll_help + + +class SandboxSetupError(Exception): + """ When sandbox has error during setup """ + pass + + +class SandboxTeardownError(Exception): + """ When sandbox has error during setup """ + pass + + +class CommandFailedException(Exception): + pass + + +# TODO: +# add context manager methods +# review design of class +# add logger +# publish env vars + + +class SandboxRestController: + def __init__(self, api: SandboxRestApiSession, sandbox_id="", log_file_handler=False): + self.api = api + self.sandbox_id = sandbox_id + self.logger = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M", + bp_params: List[InputParam] = None, permitted_users: List[str] = None, + max_polling_minutes=30) -> dict: + """ Start sandbox, poll for result, get back sandbox info """ + start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users) + sb_id = start_response["id"] + sandbox_details = poll_help.poll_sandbox_setup(self.api, sb_id, max_polling_minutes, + polling_frequency_seconds=30) + + sandbox_state = sandbox_details["state"] + stage = sandbox_details["setup_stage"] + name = sandbox_details["name"] + + if "error" in sandbox_state.lower(): + activity_errors = self._get_all_activity_errors(sb_id) + + # TODO: print this? log it? or dump into exception message? + if activity_errors: + print(f"=== Activity Feed Errors ===\n{json.dumps(activity_errors, indent=4)}") + + err_msg = (f"Sandbox '{name}' Error during SETUP.\n" + f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sb_id}'") + raise SandboxSetupError(err_msg) + return sandbox_details + + def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30) -> dict: + self.api.stop_sandbox(sandbox_id) + sandbox_details = poll_help.poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, + polling_frequency_seconds=30) + sandbox_state = sandbox_details["state"] + stage = sandbox_details["setup_stage"] + name = sandbox_details["name"] + + if "error" in sandbox_state.lower(): + tail_error_count = 5 + tailed_errors = self.api.get_sandbox_activity(sandbox_id, error_only=True, tail=tail_error_count)["events"] + print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}") + err_msg = (f"Sandbox '{name}' Error during SETUP.\n" + f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'") + raise SandboxTeardownError(err_msg) + return sandbox_details + + def _get_all_activity_errors(self, sandbox_id): + activity_results = self.api.get_sandbox_activity(sandbox_id, error_only=True) + return activity_results["events"] + + def publish_sandbox_id_to_env_vars(self): + pass diff --git a/requirements.txt b/requirements.txt index 663bd1f..e886ed4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests \ No newline at end of file +requests +retrying \ No newline at end of file From e7223620da68be2119ec264e275cd286054d18bd Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Sat, 9 Oct 2021 15:52:08 +0100 Subject: [PATCH 06/29] added state management to start / stop commands --- .../__pycache__/sandbox_api.cpython-37.pyc | Bin 16768 -> 17230 bytes .../sandbox_components.cpython-37.pyc | Bin 0 -> 5053 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 191 bytes .../polling_helpers.cpython-37.pyc | Bin 0 -> 4240 bytes .../helpers/concurrency_helper.py | 0 .../sandbox_rest/helpers/polling_helpers.py | 40 ++++- cloudshell/sandbox_rest/sandbox_controller.py | 154 +++++++++++++----- 7 files changed, 146 insertions(+), 48 deletions(-) create mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc create mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc create mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/helpers/concurrency_helper.py diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc index 1c1a3bc81671dba32478e142b90ac16aedf5cf47..9523de5f9bd52b6931ff603871c27a3ba6e0b6b9 100644 GIT binary patch delta 6152 zcmcIoZERat8TPsUc5KIS;u||j6X&B%oDbJYlO=81BrRLp!6>vD+hjIfT;H?QY5dW1 z9TY5(2Z6z#uC30Gf)Hr?fvWwhWI&T(9Rm#n2*wWt=>FJ`34sI*G$B!ekjDF-Ysc>8 zI&FnWbYGu)&%N(?&hvhp_na4Y$uD0dfm4Bi&jx?lw?38m!;L_UymMoGv3ka)I#lNu zZK{)ccI{~|+`Cja+`GlS5AHpx7w)~Z1N!~c1y6ix2R!N6m3B!w0QY`10QZ4iDIKJp zYVfkebhR@bQbXx3xWY6@L$r&A)vl}dbT{q3Hc4!>d$UIkt21G>`>K?VP?<*5o|tWy zKo@zrSB<;^t*31&Id8jQdj@{jZ3&q>$Y?c-lW6=AXsp4dg+NeSny9w4q>{8{gBz z@F$~b@pB1>?os%LH0gcS#*jUDJL{N2*QC4;vfv4JPc00O8TmmNc?mrw?)-> zX1jbTp2;FsF)VX|KITS&<1jA@1HfLZ16zI&0l1n-@X3QAsn#irwovwXA=g6N|Ber_ zh3~l@!h*G=JtfJJmT8?XWt{)T-7gPXYUTr;sWbRtg0L}E zWm4i*6mTWV6lDQ=H3@``REDf|UG+Rp#`teNW5-Nd01DkfC`)@vq)Hn=^WGn?3qTT^ zfw$9sbSvc&&8U=VCU|TF-Z+jrkx84&Lp)L?VGZsH-?N)>c!@<(44^oGVi*PLVWS}0 z(BNnEc}BHM3W7NU?+I765t&p*tcUCBc=oJN-Cir307V~ZD)W=dL2G5-?)dC5>JgD5 zCSZY+pZE8VeXOa@MJm(Q>aO_@b{KwxI+Oej|8F`3D|KgjIgqUmLYMB?)QSb&scz>> znl2SDi}Cdi<@qAZXUYm?Om~#&PC4N+oM#U8?P=D%Tg8oBK>^}~Db2=Slr{95!9kZ8CCR@R{8<=LC+zG9{IGcx3;bs1_~s+nT*ST(Hj8ca zJKa$#YGsBKV@FXOLm_Bg#ujEjhR(!JfJiu6rnG`j(Hl)u&|~~cD3-biJtCFc93B!R ze&T~^$T*F|*0U&uCPQdKmZR0yx_?JVCx0nAcV5IfVYmP#Bwb3Tbh2Zj_+P%`TRE+qE#^xr z*HAsCAeXJOXsMikJ#x6O6W(AS1);l28J5Xw35kh`%=6zzzH(mhJcQkm4O&(%Zf(;N z%N5FsO3Sd2X~J39Dk>4&H+@C8$!Y8mZh{U24mQSLmk*sqL+V~NQ_d)AE?Z`g!hPD8 z-PX#*ypq>8bT>o`Hca!=16n!5%8F*J_@C!fy;Jh2<>YFYdq?Wb5g&gu+Ph#f!N+0D zTW~cvI%TPb`(vrzJ&xD~_@GJ9dThmTL?noof&7w5sww-Fr{6gInuL?Jw7xS|lC zQPO7hN?K@1I9VE0)D9mww+|CP!m;Wxvue%6PY353Opu{ei}{v zIu?5l#r_OMaBdji#R8NX3co$%Ca?3?h7QP6mUwG_8oEjvfIl}pzO|P)4KKZaf(PbA z{uF@EBf_@k3&QF~3u?oHKj)#5m^@~Qi?57Kle7H&rCxr0rk8I{#$2ik%6T{c{>X8% z!2dZiQazXOG#4ZTt_U0aApG$+P^BT1pLII?O`DD%&P`$ROr=tGYxg7WgZp zZvmK%v2l~3bvtCkmgR6Su*G88SmCqCmawNVZ`*JVU~1KjoJO^G5gqPz5S<^^`7j{t z_k-70tWzByKUi~N&aRB|D`PVM$H9mRE~C)f9&W3J1zbd$cEiTWDhGGBplvJz@4*K~ zWdfK(B!r~)AY$wKvUcmxrW8QPMn)x2iVP-ZQ>MIPLfx!6Fv1cLNW7XjFk~XwWcVK5 zHQ7V%@?RyUB7MwTLgaT+z4#@6ckX?|&(?&W{q4xfhlA{H zSttZ=3@O`(ysPt*b00>uVtI!^aFBcOF7Jo;hX&ROf%UJ8efJ4kQ%pRN-$h#^7tV~z zY}+`ndAgP29wST!U^4>=&$0FS*FSyfh_GSd;scrJLZc%F+0J_%tPB$*{dJwkZP#k;T1@gH}QA@D+?=tm8tY zVpSu+@>~1hs2zjHzC3wh|KKs9si$O7U0XmBrTQ0mbve?eYT~ahe-p+%dwlNUSg4PS zJ3GVgO;m_I6y2fOZ9MmDES}ep5s}xxfd+sP36DyQdM;#~YqqQgo)a$tw3{bpTA-c6 z0Uv0UY6~r7Y@^}msgCZZC8|SBubFFEqmt&YVfWjEpduhnGZ`ct!hDFAR~E;t4m!K= z-9sqGQB3osX(C{ieJjZS;u92e?gjT$>mtlwFXkFD;s9P$Z6 zbYM?6h++Pn)kWgr@2w6c@g9%=@)f7(&b<*Du}f2NcxY%nqQ5`ST>Z%+5_9Y!0cC1_#Y_f!V9(2p@zWGny=7#`;c xOw7f9y>>d9c4}J`_Sj9&F`)CclS9MykQ4vIzF^gc{~~2NhyzYfTrNCI`423T6KDVc delta 5607 zcmcIoTWlOx8Q!z@&R)IVi}&Ws`kL6z+Uxbkaa_l7yfIBFq*X3$Q!Ndn&Cb-`WN)0A zMP^mifKe5-3axbzZ3Rdv5`riXJgg810e#@|z@s@cg zZi0kY^LuvZ%sJor{_p?KIsaQPkk41H?neB&6SGmMRRZM z>m5$QZjk9gtwT|%)-8&eVzH!Vuz@0qH%!z6lcyf`9?1o)aBGK45x_Vsoy10{wf3*h zggMYULaLlw&FAEzT1#19BSH{62(kBUFL?A@{2oTr¬*h&}**_kz)EqL?e@R8fqu zAKLo@11->ZI%a>j$4=pUlZ1}qn{9pAMG=OVsVI)aQ^gF%M5(}%fg49DX9#W@bgOsAl6+LfuO>*Bhckz}g0fuqCFB92`mizdNFZ072O zA+)ILnax3HLqll#(Db7jLWAAV5iomj;G*)gR94nS#PSfdQ#!$zNRU*AjYf;Nw$Bo5 z3xDiG*QrN_;AWqpD3W{}uKEZrQXMlIfnDu-PDI2MXU-?J2lk-&*-SnsWmK7?Z!no^ zjvX7?T!Oenz;po(I+DABF5;a9%`r4fU?w~DXyAjPL+*jg{^^Y- zo`5F`%GK0qBW~g%4zoalW;dzEjqu0Wf4q_I$03c_)Y28XDCQ*Kwd}hr@F~o(_kGdL zCSpO7^D@L9$IQLG8!1ekbG!oMQw|cL=$v8#pfg~ya49_RLXPnf2LXRi8G_3dQ7X0P z{e9%1W~K6HDv;xJa_6u&?t6@y205$j&7RTuwlR^y%Q&VUBNH9fA*0c1k-$5I*w~?= zo)8L6{DJnZ67Xl{2;^b8F5I$Vu; zr;K`Gv;7ga6$(T;=+5lvIYhlP^_jk9U0_l8nVv_p|MZ(Px9ej2B))^Xr>D^%!@5SE z#Jg2APodd80WGXkQI;vY-PcWCX8-OR3``r%qBh%qf$Yt~ccAkI3rj}tvs(iRp`Hz# z(fE-BqT$nR&a~-cqp-4pkBiaaO&*lZX|>JBPbadY81Y8iV zFoax~7%xn5gH9$bGz~pyweG>k_95bXI95FjmJNxxF_df&@u#6gi{I~;h`P%W4HeOR z3JofXUPQA$K>?f_Mr^4FTL_W8GHfN^Wv>kn1P&R&t^I2F8foDD^yqk=JFW?pXTkj) zXVE3f@t#G1dpgf8Z>VxnYWVNR?CxkJFlq#g4UJ7P&(aC@(wLozQ%?4^v14S8{b6im zbJE^iUi1XQWo95LzTz~}HHXIorA<*TMJ<&ZkbVvL^}KDik*F@A-MbBIeQ(IB2YwES zRt}E0xmfF1lT|&bEg~B`v8=-pbhdDT&^^f=yJrMbz=MF(^S=v>4NgZU z?XVeb7nL!TOVfV@O{L;1=l4g@rm0ph@nm5rC-!!HDB(pc$PRia%r_?BBNR}=h@3ck zH{QoqW^Mwk+cU==3|4(N*Ni-)B`V0yCIe=rpKT`sLBlO^99yv8C4K`KuFpR4^aD{- z4>yj1CsR(%$6@~WEST(ca)caUrDPQHB$x6PxlD6KRVlQlE8IqTNOiOglSQwO z{N-2&foS;a(jlF>Jr&oMwd+SHzN6$3O{fZbUR#J;$L2Q=0XwCs(CeX#sx0P_5a=@wd2*!_L2Bzpq! z=tom1>&V#(R=8d?#B`wPtXXxZ<`i##84i?w#MA@k6b*odwfH1^X65K6o?NLD4LX2! zp}}RH9zb&tO$>|%dnc&0Wec*pUXrvj651#wkH&RE5o!v?e#_>BA+zJ^eZ-{BAb7V95 zb*MO`Eg7khE5hb4h+T%zgxSc+kxd*&Gv}nqFyD`9oocQiYl8Z@a;~^WISN#9+KH#k zY(AqXP&Mhm_wl5h9?9lQ6-il_^ZB@c*uHH(s$A==jPN2_uP3Wm>?S~gSqgqeGFaF}>Hd=B@f2|sYU>BJ9IIs}2&J)rvW EUud$d+W-In diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1840cfc278748ee38bd9e3c24ab13b2c07d34784 GIT binary patch literal 5053 zcmb_g&u`qu73S|$a(3xVB{{D3A(i-P8c9fTcLI78NeJ zn&H~gmcCR5+Di{T^i&71j{Ph8Zxp=tlz$<;^m{`tclE=K4U`Mc&Ld~u%)D>j_ulZ{ z++0h;we-*52md*tY5yi+d|9a6!7VpXFpcT4=Fz_q>wUvBbgG-N*|$7P*FMvj#q8%A zv$?frdNyQ-H6S;X>_Bd^8OSqAZa{9aS;(_{hSy~JL#^BX7UO7ZUA?eAk7P#w50YU& zq^ILcN97J~c@_oNJe_Ht!F11LhG#L1%PeTJi_U7=AEXISvfu0uxb$Z44hC@)23eFQ zMf2BOrb7{OuknzJCsD}Px=zvb{Uqpf-!EFe-%r^vrh41=w}(Moy=ex6fuBKRF()b= zrP!9GA|?Gp6KzaS72j>G-dz6@Z{>OtWLdQG^?H~xzD`P`^?oEncOcR&9%gd=VUVzm zbm!{bFMe_Li`{H9P1eIW9WuGe<9J<`Z+#4$)j~L4rd%293PQ{w>rm0uE&SB8<4w4L zMn_Eu^I647nCtVX$5(g5n3oo8-sgij-Q~<{eVMSTzSgw`jU^gXG*J}I@w;MnnD}F1 zVvw>nN9{W($WW5zP199Rj-Tv#G&wq1!rOD0jY&I2=WdpXXk(aFGj438Y0LxQJqV7&N)mq@t9{`SnO?f7Ck%l zyK;t$=(ohq;mwJ$PBrWE>>Z_RUb*+X#p!B;!~L?_KP5SQYqF0+14f{W{^?%DEk8k# z>!O+KiSby=HD6PuuFCR>sp?j)ZE500uD!JPHBb~0i!u6J%dM`c=SGRlf+UNAj5GIX zlx@0`(cNs5C+;8}3}d`@WAKoi?wh39m9kA3#Id_9cn|)#=}$B)yFoAG!hOu)?a5=8 z1zF(sMA~;N2d>q*#5x_Qnd)6MDNTww()a;J#)W6Y%V}V*3cZ2dyy?jpt1Q=)Sf`z?h@!ZV-likV$tV1rTx*%K$*VbQ%*7pW7S4+~o_30d&#k zAm(@rwvlHzygewYsE~xh^{W)g@Oq; zeb>50*i(bsmq|Yj=4hk)udqxniUIl*0RG0Vvdqe@IK@_xw^11@#At6rUDpur(4h6m z<5?(>mi$-h{9rsG%@|#K(+RyPL~G%(_P&BSD0{y*ws)p&>03r=bEY4$zaf5rE?%1= zXpGPj?#8@ps_-Oc@TRbz1o4o=D9ezmWc7os#WJb|5#h=GIIlw<>wIlG_y(UU7*kT$!Z;^jCX;gmY!7EBPQ0 zHS>X?=EVCLyM&2SMM0ZS5OIiEDku*V@LF11-@Hmn@ zb(GqFf-Wjo(Lj}>%g0akXt#rONttAcwAv?7NhsGy)3)BZP;w-=%*c`5E@CC{t7dLI z)V7_eAA>(tIng)4^}Dj3(z?@_ow zBBAnl=~9TeJ4x}S8$#Vsas=Hz3QC~ek6bm)T1D+@G}Si1K&3|+!Z_M(dte;c=Qy5FKgw3O@nx{mPh%-Xq^>-V*x=6^} z&EN@lL0(Z3#zV$Sm;rY$=tuFcYAgpq$lanX!k@jKxpPSf!eA-h+q%nSl_J?4&Krnp#eJ~u+bbwz70OcbzpBzz-NIb{+ z1O~+!Z(pZ0N?y-!*2@s$@dlq${$l)H`^MPQ|A>SZhIQ5iWd+JKBkvMTSsi!fCAZJ} z8we}p4ZU=juod^0Lz&ft`spNlo_sKvI&_t%6rx|wZnnN9=SK47AY$MP3cd%JIL#C@ z4|XV3r!OehmPBc@VSgYIM9BdPo8}`AnOfoXJG_?aJCi}5Bs#c9uOP$M@I#8>Cl8(4 z1yOimu_(;M1*)1jsF#_G%1tQPi!K$Ew-#+20JA9Ub9`fAVuhanlnM%l5m zD^8=JbVq%AYU&Gf3s%$aG#0FR+tAN9&bO9YhOSz5X9V$Y;o!u9Ghj74j_(D69J0)& k=-j2g1*k4lcB@A$nzzdDP`3$oNza=UrOkO~-kxv#7tadp?f?J) literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b20b3f99c96cfe3923b9169a2ea40bf89947b6b GIT binary patch literal 191 zcmZ?b<>g`kf+;8C5<&E15CH>>K!yVl7qb9~6oz01O-8?!3`HPe1o11w*(xTqIJKxa zCNHt1BvUUtCOJPPHKwpMF()%7H?ufdp`a)~D>b>KI3_qTFC{6zLf0|CQ#YWpBqKjB zCOId+G^IErH76&g7$O^Alv-R80~9F$nHe9SnU`4-AFo$Xd5gm)H$SB`C)Ez*g3mzA F007H&HckKl literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18e0a2b4aa9cb546cee1bcb3c1a4cdae242d1c9f GIT binary patch literal 4240 zcmb7HTXWmS6~^L1Qj|o|y4aR%$&jyQ6U*s!rk*rYTcH!pbY#VpTo=PB3Sn0gBoJU0 zKrCsbm&!BoOJDm^ccyvCO#TvH`%?EO^eNw2kQAjzofMp1?%7?OJ=gCno{o=KH9TK@ z>v?~>qG|skqW_dJ_#ImEOQvaDb2QGl9|R&&te6uwcD(O9SNJ68c~KDy!VtIc zTi{dgN_?8nVBY3;)yf>d{?6dDeD00qEb@Cpb2s=+%-vS}=D~k~-@AFiyZlZ|Ydkpow)(jUyF%JqS>&fd93^%d+ub+}?Q}=jN$RD-j<@W@i}+@|Zzm$n zx^^tps@NBPMlY#-Swb5<+mAaP>|UyFBtf)oKae7o2N*rD!(dZ-@_KLqppm1g`Ym^zxk;8o_yZWbMyB>lIE3VFATlSP+-PVoAT-lI;AOP zEE_DJX%*tW5J~!^8?;1{KuRo_%_!^m#H{}ybc!Gv!$4>bLm=s#!8m$;UABM~xb zM_!r+OM7iU=AuozgLWrKe7mQ4((Wnn@X6ZB!?lBSCyv^F7-u}$5n?{DIzbgfybOl%v!Q6f8#LG*?4cN9&(V z^u;IN#mcGO9fRwR$qmQirc>gUQ|2Y7!prbSB_HourA1MhIHOO+RxCw}LejC+Uv8-o zaI6=?;|EShG0^T|!c#8rXqs(8<*WEMqa%HhdTLBZAAF)E3F zkEo@+Q~Ha7&MJrtjWYq2r^z^Exe7A}KS?VWo9QN2E;kd*Q_8P2`}s%)$`Y8U{G!mr zDX&2&sN{^mtKCqff;**WUJ#N|%IA$z8yG+_%4^-?ZKYF|ICbQB!K6PkzNL8e$P2Z; z1`X!X5{k;c){^_IpB7{xra?EvVN1^~1GIHllgPl&Fs}XAM)CU!u%tx%HJ7 zWOS{h!5mM zooL6UKWNciQ%l)iRlZ;v{ZgHmj*P#fzW>8GVSizN)FSf1tXNPpEq1#u0&U7N&XiwHKuBRyqYNgxoqA7UQjKhzV zsRtZXL@#{|g}gBNWa$FthadE?)LtvfhMl6WaT}G*_O?=72;i2jU^|nTJgnF~4RmHjVC3M+quP$kVVpqlwCysSAYuE#66}SZG$2&6zr@vKqU}WCJtr z2{YeQ=0FS^f5oXPa!4wANHB`mWweC$BXR%=DMS1iC)TlfWRlGfuN8NkECMpnjfK#R zE;5jkj#ByAu!c@KRg1vjd?HO!%$%B^P-c}RP^n{7S&7LHLHqx&nAa~6b0DWm$R97~ z%o#cRS^n~zs1HE{lPhB1ce$5(MRHz<#HVNl1ZJrQ^c_O0)x1QZm?eNDmd|gjK3iE` zX?@=O)NQ_ME^n-_JYRKN>rd7N?kg=c>BDy7cYaY!G-isq!A&A z#wz#$xqgN*(s!(%WOQ}Pr{Q_JtKzKQO%^~_XxsM`d2j$lZ4fD-1t3IFq+bF>=0WYq ze9PWyC;G9~gCiYq^s)y>2H;5l0UTKhj=recc*srmKqwp69Q*d5aNq7-w?Cvycw`ra z^4x_SdECuX#cLS8_q(PamXa2uB7&0pe7p zMrk3x!F|8LpZqy>1E9($&%J|a3h4}GUL?lhZ&XB{a)qkyX+ECCu+v=>m52A6eXk85 z`#gvNiigq5EYOQ$p@1+#nntM*n#6am4wtPUa(lSv+$1sSO1L$sTv+WFuU`>EvVzk5 zj46uS!T(fJ;6-J0tqjbwbReF>JSx7cVD?jD`~kX4$M^EG>+;yg*GDPES5&ku={_OJ zrA|dvESe*uE4}kOrymnb!x=FkP~atner^hUF-a=O`ABo$7hQr$1^w!dU)(e35~uD> z>awA3P5E@|$?B(1pTBZj&Gn5nmFCTsBuJJNW%x$xtv^{_UwPT=Zy-RCv($Y=yG$PV zX>M-Dai}m^-9T~!-(xm1g(-lOJ%tvyA46cX)snkEFOf0Nbm8)IWe a-NbLCUM{h!-fKp^LiA)^W7B17^~(3kv>q$~ literal 0 HcmV?d00001 diff --git a/cloudshell/sandbox_rest/helpers/concurrency_helper.py b/cloudshell/sandbox_rest/helpers/concurrency_helper.py deleted file mode 100644 index e69de29..0000000 diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py index d56bf59..921bfc5 100644 --- a/cloudshell/sandbox_rest/helpers/polling_helpers.py +++ b/cloudshell/sandbox_rest/helpers/polling_helpers.py @@ -4,11 +4,19 @@ https://pypi.org/project/retrying/ """ from typing import List, Callable -from retrying import retry # pip install retrying -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiClient +from retrying import retry, RetryError # pip install retrying +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession from enum import Enum +class OrchestrationPollingTimeout(Exception): + pass + + +class CommandPollingTimeout(Exception): + pass + + class SandboxStates(Enum): before_setup_state = "BeforeSetup" running_setup_state = "Setup" @@ -25,6 +33,12 @@ class ExecutionStatuses(Enum): failed_status = "Failed" +SANDBOX_SETUP_STATES = [SandboxStates.before_setup_state.value, + SandboxStates.running_setup_state.value] + +SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value, + SandboxStates.error_state.value] + UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value, ExecutionStatuses.pending_status.value] @@ -44,7 +58,7 @@ def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool: return False -def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_func: Callable, +def _poll_sandbox_state(api: SandboxRestApiSession, reservation_id: str, polling_func: Callable, max_polling_minutes: int, polling_frequency_seconds: int) -> str: """ Create blocking polling process """ @@ -54,10 +68,14 @@ def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_ def get_sandbox_details(): return api.get_sandbox_details(reservation_id) - return get_sandbox_details() + try: + sandbox_details = get_sandbox_details() + except RetryError: + raise OrchestrationPollingTimeout(f"Sandbox Polling timed out after configured {max_polling_minutes} minutes") + return sandbox_details -def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20, +def poll_sandbox_setup(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30) -> dict: """ poll until completion """ sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, @@ -65,7 +83,7 @@ def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polli return sandbox_details -def poll_sandbox_teardown(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20, +def poll_sandbox_teardown(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30) -> dict: """ poll until completion """ 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: return False -def poll_execution_for_completion(sandbox_rest: SandboxRestApiClient, command_execution_id: str, +def poll_execution_for_completion(sandbox_rest: SandboxRestApiSession, command_execution_id: str, max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str: """ poll execution for "Completed" status, then return the execution output """ + # retry wait times are in milliseconds @retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000, stop_max_delay=max_polling_in_minutes * 60000) def get_execution_data(): exc_data = sandbox_rest.get_execution_details(command_execution_id) return exc_data - return get_execution_data(sandbox_rest, command_execution_id) - + try: + exc_data = get_execution_data(sandbox_rest, command_execution_id) + except RetryError: + raise CommandPollingTimeout(f"Execution polling timed out after max {max_polling_in_minutes} minutes") + return exc_data diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py index 1e52f33..93f4b64 100644 --- a/cloudshell/sandbox_rest/sandbox_controller.py +++ b/cloudshell/sandbox_rest/sandbox_controller.py @@ -1,9 +1,10 @@ +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam +from cloudshell.sandbox_rest.helpers.polling_helpers import poll_sandbox_setup, poll_sandbox_teardown, \ + poll_execution_for_completion, SandboxStates, SANDBOX_SETUP_STATES, SANDBOX_ACTIVE_STATES +from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents import json -import logging from typing import List - -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam -from cloudshell.sandbox_rest.helpers import polling_helpers as poll_help +from timeit import default_timer class SandboxSetupError(Exception): @@ -20,70 +21,145 @@ class CommandFailedException(Exception): pass -# TODO: -# add context manager methods -# review design of class -# add logger -# publish env vars - - class SandboxRestController: - def __init__(self, api: SandboxRestApiSession, sandbox_id="", log_file_handler=False): + def __init__(self, api: SandboxRestApiSession, sandbox_id="", disable_prints=False): self.api = api self.sandbox_id = sandbox_id - self.logger = None + self.components = SandboxRestComponents() + self.setup_finished = False + self.sandbox_ended = False + self.setup_errors: List[dict] = None + self.teardown_errors: List[dict] = None + self.disable_prints = disable_prints + + # when passing in existing sandbox id update instance state, otherwise let "start sandbox" handle it + if self.sandbox_id: + self._handle_sandbox_id_on_init() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - pass + if exc_type: + err_msg = f"Exiting sandbox scope with error. f{exc_type}: {exc_val}" + self._print(err_msg) + + def _handle_sandbox_id_on_init(self): + self.update_components() + self.update_state_flags() + + def update_components(self): + components = self.api.get_sandbox_components(self.sandbox_id) + self.components.refresh_components(components) + + def update_state_flags(self): + details = self.api.get_sandbox_details(self.sandbox_id) + sandbox_state = details["state"] + if sandbox_state not in SANDBOX_SETUP_STATES: + self.setup_finished = True + elif sandbox_state not in SANDBOX_ACTIVE_STATES: + self.sandbox_ended = True + + def _print(self, message): + if not self.disable_prints: + print(message) def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M", bp_params: List[InputParam] = None, permitted_users: List[str] = None, - max_polling_minutes=30) -> dict: + max_polling_minutes=30, raise_setup_exception=True) -> dict: """ Start sandbox, poll for result, get back sandbox info """ - start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users) - sb_id = start_response["id"] - sandbox_details = poll_help.poll_sandbox_setup(self.api, sb_id, max_polling_minutes, - polling_frequency_seconds=30) + if self.sandbox_id: + raise ValueError(f"Sandbox already has id '{self.sandbox_id}'.\n" + f"Start blueprint with new sandbox controller instance") + self._print(f"Starting blueprint {blueprint_id}") + start = default_timer() + start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users) + self.sandbox_id = start_response["id"] + sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes, + polling_frequency_seconds=30) + self.update_components() sandbox_state = sandbox_details["state"] stage = sandbox_details["setup_stage"] name = sandbox_details["name"] - if "error" in sandbox_state.lower(): - activity_errors = self._get_all_activity_errors(sb_id) + if sandbox_state == SandboxStates.ready_state.value: + self.setup_finished = True - # TODO: print this? log it? or dump into exception message? + # scan activity feed for errors + if sandbox_state == SandboxStates.error_state.value: + self.setup_finished = True + activity_errors = self._get_all_activity_errors(self.sandbox_id) if activity_errors: - print(f"=== Activity Feed Errors ===\n{json.dumps(activity_errors, indent=4)}") - - err_msg = (f"Sandbox '{name}' Error during SETUP.\n" - f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sb_id}'") - raise SandboxSetupError(err_msg) + # print and store setup error data + self.setup_errors = activity_errors + err_msg = f"Error Events during setup:\n{json.dumps(activity_errors, indent=4)}" + self._print(err_msg) + + if raise_setup_exception: + err_msg = (f"Sandbox '{name}' Error during SETUP. See events for details.\n" + f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'") + raise SandboxSetupError(err_msg) + + total_minutes = (default_timer() - start) / 60 + self._print(f"Setup finished after {total_minutes:.2f} minutes.") return sandbox_details - def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30) -> dict: + def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_teardown_exception=True) -> dict: + if self.sandbox_ended: + raise ValueError(f"sandbox {self.sandbox_id} already in completed state") + + last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"] + start = default_timer() self.api.stop_sandbox(sandbox_id) - sandbox_details = poll_help.poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, - polling_frequency_seconds=30) + sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, + polling_frequency_seconds=30) + self.update_components() sandbox_state = sandbox_details["state"] stage = sandbox_details["setup_stage"] name = sandbox_details["name"] - if "error" in sandbox_state.lower(): - tail_error_count = 5 - tailed_errors = self.api.get_sandbox_activity(sandbox_id, error_only=True, tail=tail_error_count)["events"] - print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}") - err_msg = (f"Sandbox '{name}' Error during SETUP.\n" - f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'") - raise SandboxTeardownError(err_msg) + if sandbox_state == SandboxStates.ended_state.value: + self.sandbox_ended = True + + tail_error_count = 5 + tailed_errors = self.api.get_sandbox_activity(sandbox_id, + error_only=True, + from_event_id=last_activity_id + 1, + tail=tail_error_count)["events"] + if tailed_errors: + self.sandbox_ended = True + self.teardown_errors = tailed_errors + self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}") + + if raise_teardown_exception: + err_msg = (f"Sandbox '{name}' Error during SETUP.\n" + f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'") + raise SandboxTeardownError(err_msg) + + total_minutes = (default_timer() - start) / 60 + self._print(f"Teardown done in {total_minutes:.2f} minutes.") return sandbox_details + def rerun_setup(self): + pass + + def run_sandbox_command_and_poll(self): + pass + + def run_component_command_and_poll(self): + pass + def _get_all_activity_errors(self, sandbox_id): - activity_results = self.api.get_sandbox_activity(sandbox_id, error_only=True) - return activity_results["events"] + return self.api.get_sandbox_activity(sandbox_id, error_only=True)["events"] def publish_sandbox_id_to_env_vars(self): + """ publish sandbox id as environment variable for different CI process to pick up """ pass + + +if __name__ == "__main__": + api = SandboxRestApiSession("localhost", "admin", "admin") + controller = SandboxRestController(api) + response = controller.start_sandbox_and_poll("rest test", "lolol") + print(json.dumps(response, indent=4)) From faa1d66ac0ce7f9a51f2c8e9418e3d8470f6b11b Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Fri, 22 Oct 2021 15:42:35 +0300 Subject: [PATCH 07/29] basic tests added --- .pre-commit-config.yaml | 44 ++++ build/lib/cloudshell/__init__.py | 5 +- build/lib/cloudshell/sandbox_rest/__init__.py | 5 +- .../cloudshell/sandbox_rest/sandbox_api.py | 147 ++++++------ cloudshell/__init__.py | 5 +- cloudshell/sandbox_rest/__init__.py | 5 +- .../sandbox_rest/helpers/polling_helpers.py | 69 +++--- cloudshell/sandbox_rest/sandbox_api.py | 221 +++++++++--------- cloudshell/sandbox_rest/sandbox_components.py | 18 +- cloudshell/sandbox_rest/sandbox_controller.py | 63 +++-- setup.py | 28 +-- tests/common.py | 10 + tests/common_fixtures.py | 20 ++ tests/test_api _no_sandbox.py | 35 +++ tests/test_api_with_sandbox.py | 33 +++ tests/test_rest_client.py | 51 ---- tests/test_sandbox_controller.py | 0 17 files changed, 454 insertions(+), 305 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 tests/common.py create mode 100644 tests/common_fixtures.py create mode 100644 tests/test_api _no_sandbox.py create mode 100644 tests/test_api_with_sandbox.py delete mode 100644 tests/test_rest_client.py create mode 100644 tests/test_sandbox_controller.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e3d0d87 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,44 @@ +repos: + - repo: https://github.com/timothycrosley/isort + rev: 5.4.2 + hooks: + - id: isort + language_version: python3.7 + args: [--line-length=127] + - repo: https://github.com/python/black + rev: 20.8b1 + hooks: + - id: black + language_version: python3.7 + args: [--line-length=127] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.3 + hooks: + - id: flake8 + additional_dependencies: [ + flake8-docstrings, + flake8-builtins, + flake8-comprehensions, + flake8-print, + flake8-eradicate, + ] + language_version: python3.7 + args: [ + --max-line-length=127, + '--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D210,D401,W503,E203' + ] + # See https://stackoverflow.com/questions/61238318/pylint-and-pre-commit-hook-unable-to-import/61238571#61238571 + - repo: local + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: [ + --max-line-length=127, + --max-public-methods=32, + --max-args=8, + '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding', + '--good-names=ip,rc,eval' + ] \ No newline at end of file diff --git a/build/lib/cloudshell/__init__.py b/build/lib/cloudshell/__init__.py index b50dc4a..cee05d2 100644 --- a/build/lib/cloudshell/__init__.py +++ b/build/lib/cloudshell/__init__.py @@ -1,3 +1,4 @@ -__author__ = 'quali' +__author__ = "quali" from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) \ No newline at end of file + +__path__ = extend_path(__path__, __name__) diff --git a/build/lib/cloudshell/sandbox_rest/__init__.py b/build/lib/cloudshell/sandbox_rest/__init__.py index b50dc4a..cee05d2 100644 --- a/build/lib/cloudshell/sandbox_rest/__init__.py +++ b/build/lib/cloudshell/sandbox_rest/__init__.py @@ -1,3 +1,4 @@ -__author__ = 'quali' +__author__ = "quali" from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) \ No newline at end of file + +__path__ = extend_path(__path__, __name__) diff --git a/build/lib/cloudshell/sandbox_rest/sandbox_api.py b/build/lib/cloudshell/sandbox_rest/sandbox_api.py index ecc62ef..6b1bf2b 100644 --- a/build/lib/cloudshell/sandbox_rest/sandbox_api.py +++ b/build/lib/cloudshell/sandbox_rest/sandbox_api.py @@ -1,18 +1,20 @@ import json +from dataclasses import asdict, dataclass from typing import List import requests -from dataclasses import dataclass, asdict class SandboxRestException(Exception): """ General exception to raise inside Rest client class """ + pass @dataclass class InputParam: """ To model the param objects passed to sandbox / component command endpoints """ + name: str value: str @@ -23,8 +25,7 @@ class SandboxRestApiClient: View http:///api/v2/explore to see schemas of return json values """ - def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False, - api_version="v2"): + def __init__(self, host: str, username: str, password: str, domain="Global", port=82, is_https=False, api_version="v2"): """ login to api and store headers for future requests """ protocol = "https" if is_https else "http" self._base_url = f"{protocol}://{host}:{port}/api" @@ -35,19 +36,18 @@ def _get_auth_headers(self, user_name: str, password: str, domain: str): """ Get token from login response, then place token into auth headers on class """ - login_res = requests.put(url=f"{self._base_url}/login", - data=json.dumps({"username": user_name, "password": password, "domain": domain}), - headers={"Content-Type": "application/json"}) + login_res = requests.put( + url=f"{self._base_url}/login", + data=json.dumps({"username": user_name, "password": password, "domain": domain}), + headers={"Content-Type": "application/json"}, + ) if not login_res.ok: raise SandboxRestException(self._format_err(login_res, "Failed Login")) login_token = login_res.text[1:-1] - auth_headers = { - 'Authorization': f'Basic {login_token}', - 'Content-Type': 'application/json' - } + auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"} return auth_headers @staticmethod @@ -58,20 +58,26 @@ def _format_err(response: requests.Response, custom_err_msg="Failed Api Call"): return err_msg # SANDBOX POST REQUESTS - def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", - bp_params: List[InputParam] = None, permitted_users: List[str] = None): + def start_sandbox( + self, + blueprint_id: str, + sandbox_name="", + duration="PT2H0M", + bp_params: List[InputParam] = None, + permitted_users: List[str] = None, + ): """ Create a sandbox from the provided blueprint id Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') """ - url = f'{self._versioned_url}/blueprints/{blueprint_id}/start' - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + url = f"{self._versioned_url}/blueprints/{blueprint_id}/start" + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] data_dict = { "duration": duration, "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], - "params": [asdict(x) for x in bp_params] if bp_params else [] + "params": [asdict(x) for x in bp_params] if bp_params else [], } response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) @@ -80,16 +86,17 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", raise SandboxRestException(err_msg) return response.json() - def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, - permitted_users: List[str] = None): + def start_persistent_sandbox( + self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None + ): """ Create a persistent sandbox from the provided blueprint id """ - url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent' + url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] data_dict = { "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], - "params": [asdict(x) for x in bp_params] if bp_params else [] + "params": [asdict(x) for x in bp_params] if bp_params else [], } response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) @@ -98,10 +105,9 @@ def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params raise SandboxRestException(err_msg) return response.json() - def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, - print_output=True): + def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True): """ Run a sandbox level command """ - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params @@ -110,17 +116,17 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'")) return response.json() - def run_component_command(self, sandbox_id: str, component_id: str, command_name: str, - params: List[InputParam] = None, print_output: bool = True): + def run_component_command( + self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True + ): """ Start a command on sandbox component """ - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"failed to start component command '{command_name}'")) + raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'")) return response.json() def extend_sandbox(self, sandbox_id: str, duration: str): @@ -130,16 +136,16 @@ def extend_sandbox(self, sandbox_id: str, duration: str): :return: """ data_dict = {"extended_time": duration} - response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend', - data=json.dumps(data_dict), - headers=self._auth_headers) + response = requests.post( + f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers + ) if not response.ok: raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'")) return response.json() def stop_sandbox(self, sandbox_id: str): """ Stop the sandbox given sandbox id """ - response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers) + response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) return response.json() @@ -150,20 +156,19 @@ def get_sandboxes(self, show_historic=False): :return: """ params = {"show_historic": show_historic} - response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers, params=params) + response = requests.get(f"{self._versioned_url}/sandboxes", headers=self._auth_headers, params=params) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list")) return response.json() def get_sandbox_details(self, sandbox_id: str): """ Get details of the given sandbox id """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'")) return response.json() - def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, - tail: int = None): + def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None): """ Get list of sandbox activity 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') @@ -171,7 +176,7 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from 'tail' - how many of the last entries you want to pull 'error_only' - to filter for error events only """ - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity" params = {} if error_only: @@ -194,24 +199,26 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from def get_sandbox_commands(self, sandbox_id: str): """ Get list of sandbox commands """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'")) return response.json() def get_sandbox_command_details(self, sandbox_id: str, command_name: str): """ Get details of specific sandbox command """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}', - headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers + ) if not response.ok: - err_msg = self._format_err(response, - f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'") + err_msg = self._format_err( + response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'" + ) raise SandboxRestException(err_msg) return response.json() def get_sandbox_components(self, sandbox_id: str): """ Get list of sandbox components """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers) if not response.ok: err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'") raise SandboxRestException(err_msg) @@ -219,22 +226,26 @@ def get_sandbox_components(self, sandbox_id: str): def get_sandbox_component_details(self, sandbox_id: str, component_id: str): """ Get details of components in sandbox """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}', - headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers + ) if not response.ok: - custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', " - f"sandbox: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + ) err_msg = self._format_err(response, custom_err_msg) raise SandboxRestException(err_msg) return response.json() def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): """ Get list of commands for a particular component in sandbox """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands', - headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers + ) if not response.ok: - custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', " - f"sandbox: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + ) err_msg = self._format_err(response, custom_err_msg) raise SandboxRestException(err_msg) return response.json() @@ -242,28 +253,28 @@ def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str): """ Get details of a command of sandbox component """ response = requests.get( - f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}', - headers=self._auth_headers) + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}", + headers=self._auth_headers, + ) if not response.ok: - custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', " - f"sandbox: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + ) err_msg = self._format_err(response, custom_err_msg) raise SandboxRestException(err_msg) return response.json() def get_sandbox_instructions(self, sandbox_id: str): """ pull the instruction text of sandbox """ - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions', - headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, - f"Failed to get sandbox instructions for '{sandbox_id}'") + err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") raise SandboxRestException(err_msg) return response.json() def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None): """ Get list of sandbox output """ - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions" params = {} if tail: params["tail"] = tail @@ -278,15 +289,14 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i response = requests.get(url, headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, - f"Failed to get sandbox instructions for '{sandbox_id}'") + err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") raise SandboxRestException(err_msg) return response.json() # BLUEPRINT GET REQUESTS def get_blueprints(self): """ Get list of blueprints """ - response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, "Failed to get blueprints")) return response.json() @@ -298,27 +308,24 @@ def get_blueprint_details(self, blueprint_id: str): """ response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'") if not response.ok: - raise SandboxRestException( - self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) + raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) return response.json() # EXECUTIONS def get_execution_details(self, execution_id: str): response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"Failed to get execution details for '{execution_id}'")) + raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'")) return response.json() def delete_execution(self, execution_id: str): response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"Failed to delete execution for '{execution_id}'")) + raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'")) return response.json() -if __name__ == '__main__': +if __name__ == "__main__": api = SandboxRestApiClient("localhost", "admin", "admin") bps = api.get_blueprints() pass diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py index b50dc4a..cee05d2 100644 --- a/cloudshell/__init__.py +++ b/cloudshell/__init__.py @@ -1,3 +1,4 @@ -__author__ = 'quali' +__author__ = "quali" from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) \ No newline at end of file + +__path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py index b50dc4a..cee05d2 100644 --- a/cloudshell/sandbox_rest/__init__.py +++ b/cloudshell/sandbox_rest/__init__.py @@ -1,3 +1,4 @@ -__author__ = 'quali' +__author__ = "quali" from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) \ No newline at end of file + +__path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py index 921bfc5..531da57 100644 --- a/cloudshell/sandbox_rest/helpers/polling_helpers.py +++ b/cloudshell/sandbox_rest/helpers/polling_helpers.py @@ -3,10 +3,12 @@ Using 'retrying' library to do polling: https://pypi.org/project/retrying/ """ -from typing import List, Callable -from retrying import retry, RetryError # pip install retrying -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession from enum import Enum +from typing import Callable, List + +from retrying import RetryError, retry # pip install retrying + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession class OrchestrationPollingTimeout(Exception): @@ -18,8 +20,8 @@ class CommandPollingTimeout(Exception): class SandboxStates(Enum): - before_setup_state = "BeforeSetup" - running_setup_state = "Setup" + BEFORE_SETUP_STATE = "BeforeSetup" + RUNNING_SETUP_STATE = "Setup" error_state = "Error" ready_state = "Ready" teardown_state = "Teardown" @@ -33,19 +35,16 @@ class ExecutionStatuses(Enum): failed_status = "Failed" -SANDBOX_SETUP_STATES = [SandboxStates.before_setup_state.value, - SandboxStates.running_setup_state.value] +SANDBOX_SETUP_STATES = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value] -SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value, - SandboxStates.error_state.value] +SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value, SandboxStates.error_state.value] -UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value, - ExecutionStatuses.pending_status.value] +UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value, ExecutionStatuses.pending_status.value] def _should_we_keep_polling_setup(sandbox_details: dict) -> bool: """ if still in setup keep polling """ - setup_states = [SandboxStates.before_setup_state.value, SandboxStates.running_setup_state.value] + setup_states = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value] if sandbox_details["state"] in setup_states: return True return False @@ -58,13 +57,19 @@ def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool: return False -def _poll_sandbox_state(api: SandboxRestApiSession, reservation_id: str, polling_func: Callable, - max_polling_minutes: int, polling_frequency_seconds: int) -> str: +def _poll_sandbox_state( + api: SandboxRestApiSession, + reservation_id: str, + polling_func: Callable, + max_polling_minutes: int, + polling_frequency_seconds: int, +) -> str: """ Create blocking polling process """ # retry wait times are in milliseconds - @retry(retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000, - stop_max_delay=max_polling_minutes * 60000) + @retry( + retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000, stop_max_delay=max_polling_minutes * 60000 + ) def get_sandbox_details(): return api.get_sandbox_details(reservation_id) @@ -75,19 +80,23 @@ def get_sandbox_details(): return sandbox_details -def poll_sandbox_setup(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, - polling_frequency_seconds=30) -> dict: +def poll_sandbox_setup( + api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30 +) -> dict: """ poll until completion """ - sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, - polling_frequency_seconds) + sandbox_details = _poll_sandbox_state( + api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, polling_frequency_seconds + ) return sandbox_details -def poll_sandbox_teardown(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, - polling_frequency_seconds=30) -> dict: +def poll_sandbox_teardown( + api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30 +) -> dict: """ poll until completion """ - sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes, - polling_frequency_seconds) + sandbox_details = _poll_sandbox_state( + api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes, polling_frequency_seconds + ) return sandbox_details @@ -98,15 +107,19 @@ def _should_we_keep_polling_execution(exc_data: dict) -> bool: return False -def poll_execution_for_completion(sandbox_rest: SandboxRestApiSession, command_execution_id: str, - max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str: +def poll_execution_for_completion( + sandbox_rest: SandboxRestApiSession, command_execution_id: str, max_polling_in_minutes=20, polling_frequency_in_seconds=30 +) -> str: """ poll execution for "Completed" status, then return the execution output """ # retry wait times are in milliseconds - @retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000, - stop_max_delay=max_polling_in_minutes * 60000) + @retry( + retry_on_result=_should_we_keep_polling_execution, + wait_fixed=polling_frequency_in_seconds * 1000, + stop_max_delay=max_polling_in_minutes * 60000, + ) def get_execution_data(): exc_data = sandbox_rest.get_execution_details(command_execution_id) return exc_data diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 7b6803b..ae4f970 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -1,22 +1,26 @@ import json import time +from dataclasses import asdict, dataclass from typing import List + import requests -from dataclasses import dataclass, asdict class SandboxRestException(Exception): """ General exception to raise inside Rest client class """ + pass class SandboxRestAuthException(Exception): """ Failed auth actions """ + pass class SandboxRestInitException(ValueError): """ Failed auth actions """ + pass @@ -26,6 +30,7 @@ class InputParam: param objects passed to sandbox / component command endpoints sandbox global inputs, commands and resource commands all follow this generic name/value convention """ + name: str value: str @@ -36,17 +41,21 @@ class SandboxRestApiSession: View http:///api/v2/explore to see schemas of return json values """ - def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False, - api_version="v2"): + def __init__( + self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2" + ): """ login to api and store headers for future requests """ _protocol = "https" if is_https else "http" + self._base_url = f"{_protocol}://{host}:{port}/api" + self._versioned_url = f"{self._base_url}/{api_version}" self.host = host self.username = username self._password = password self.domain = domain - self._base_url = f"{_protocol}://{host}:{port}/api" - self._versioned_url = f"{self._base_url}/{api_version}" - self.auth_token = self.get_token_with_credentials(username, password, domain) + if self.username and self._password: + self.auth_token = self.get_token_with_credentials(username, password, domain) + elif token: + self.auth_token = token self._auth_headers = self._build_auth_headers(self.auth_token) # CONTEXT MANAGER METHODS @@ -62,18 +71,15 @@ def _build_auth_headers(login_token: str) -> dict: """ interpolate token into auth_headers dict """ - auth_headers = { - 'Authorization': f'Basic {login_token}', - 'Content-Type': 'application/json' - } + auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"} return auth_headers - def refresh_auth(self): + def refresh_auth_from_stored_credentials(self): self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) self._auth_headers = self._build_auth_headers(self.auth_token) def invalidate_auth(self): - self.invalidate_target_token(self.auth_token) + self.delete_token(self.auth_token) self._auth_headers = None def _validate_auth_headers(self): @@ -83,11 +89,13 @@ def _validate_auth_headers(self): # LOGIN METHODS def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str: """ - Get token from credentials - extraneous quotes stripped off token string - """ - login_res = requests.put(url=f"{self._base_url}/login", - data=json.dumps({"username": user_name, "password": password, "domain": domain}), - headers={"Content-Type": "application/json"}) + Get token from credentials - extraneous quotes stripped off token string + """ + login_res = requests.put( + url=f"{self._base_url}/login", + data=json.dumps({"username": user_name, "password": password, "domain": domain}), + headers={"Content-Type": "application/json"}, + ) if not login_res.ok: raise SandboxRestAuthException(self._format_err(login_res, "Failed Login")) @@ -100,9 +108,9 @@ def get_token_for_target_user(self, user_name: str) -> str: Get token for target user - remove extraneous quotes """ self._validate_auth_headers() - login_res = requests.post(url=f"{self._base_url}/token", - data=json.dumps({"username": user_name}), - headers=self._auth_headers) + login_res = requests.post( + url=f"{self._base_url}/token", data=json.dumps({"username": user_name}), headers=self._auth_headers + ) if not login_res.ok: raise SandboxRestException(self._format_err(login_res, f"Failed to get get token for user {user_name}")) @@ -110,37 +118,42 @@ def get_token_for_target_user(self, user_name: str) -> str: login_token = login_res.text[1:-1] return login_token - def invalidate_target_token(self, token_id: str) -> None: + def delete_token(self, token_id: str) -> None: self._validate_auth_headers() - login_res = requests.delete(url=f"{self._base_url}/token/{token_id}", - headers=self._auth_headers) + login_res = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers) if not login_res.ok: raise SandboxRestException(self._format_err(login_res, "Failed to delete token")) def _format_err(self, response: requests.Response, custom_err_msg="Failed Api Call"): - response_data = (f"Response: {response.status_code}, Reason: {response.reason}\n" - f"Request URL: {response.request.url}\n" - f"Request Headers: {response.request.headers}") - return (f"Sandbox API Error for User: '{self.username}', Domain: '{self.domain}'.\n" - f"{custom_err_msg}\n" - f"{response_data}") + response_data = ( + f"Response: {response.status_code}, Reason: {response.reason}\n" + f"Request URL: {response.request.url}\n" + f"Request Headers: {response.request.headers}" + ) + return f"Sandbox API Error for User: '{self.username}'.\n" f"{custom_err_msg}\n" f"{response_data}" # SANDBOX POST REQUESTS - def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", - bp_params: List[InputParam] = None, permitted_users: List[str] = None): + def start_sandbox( + self, + blueprint_id: str, + sandbox_name="", + duration="PT2H0M", + bp_params: List[InputParam] = None, + permitted_users: List[str] = None, + ) -> dict: """ Create a sandbox from the provided blueprint id Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') """ self._validate_auth_headers() - url = f'{self._versioned_url}/blueprints/{blueprint_id}/start' - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + url = f"{self._versioned_url}/blueprints/{blueprint_id}/start" + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] data_dict = { "duration": duration, "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], - "params": [asdict(x) for x in bp_params] if bp_params else [] + "params": [asdict(x) for x in bp_params] if bp_params else [], } response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) @@ -154,17 +167,18 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M", raise SandboxRestException(err_msg) return response.json() - def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, - permitted_users: List[str] = None): + def start_persistent_sandbox( + self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None + ): """ Create a persistent sandbox from the provided blueprint id """ self._validate_auth_headers() - url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent' + url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name'] + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] data_dict = { "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], - "params": [asdict(x) for x in bp_params] if bp_params else [] + "params": [asdict(x) for x in bp_params] if bp_params else [], } response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) @@ -173,11 +187,10 @@ def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params raise SandboxRestException(err_msg) return response.json() - def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, - print_output=True): + def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True): """ Run a sandbox level command """ self._validate_auth_headers() - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params @@ -186,18 +199,18 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'")) return response.json() - def run_component_command(self, sandbox_id: str, component_id: str, command_name: str, - params: List[InputParam] = None, print_output: bool = True): + def run_component_command( + self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True + ): """ Start a command on sandbox component """ self._validate_auth_headers() - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"failed to start component command '{command_name}'")) + raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'")) return response.json() def extend_sandbox(self, sandbox_id: str, duration: str): @@ -208,9 +221,9 @@ def extend_sandbox(self, sandbox_id: str, duration: str): """ self._validate_auth_headers() data_dict = {"extended_time": duration} - response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend', - data=json.dumps(data_dict), - headers=self._auth_headers) + response = requests.post( + f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers + ) if not response.ok: raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'")) return response.json() @@ -218,7 +231,7 @@ def extend_sandbox(self, sandbox_id: str, duration: str): def stop_sandbox(self, sandbox_id: str): """ Stop the sandbox given sandbox id """ self._validate_auth_headers() - response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers) + response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) @@ -226,26 +239,25 @@ def stop_sandbox(self, sandbox_id: str): def get_sandboxes(self, show_historic=False): """ Get list of sandboxes """ self._validate_auth_headers() - url = f'{self._versioned_url}/sandboxes' + url = f"{self._versioned_url}/sandboxes" params = {"show_historic": "true" if show_historic else "false"} response = requests.get(url, headers=self._auth_headers, params=params) if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox list") + err_msg = self._format_err(response, "Failed to get sandbox list") raise SandboxRestException(err_msg) return response.json() def get_sandbox_details(self, sandbox_id: str): """ Get details of the given sandbox id """ self._validate_auth_headers() - url = f'{self._versioned_url}/sandboxes/{sandbox_id}' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}" response = requests.get(url, headers=self._auth_headers) if not response.ok: exc_msg = self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'") raise SandboxRestException(exc_msg) return response.json() - def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, - tail: int = None): + def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None): """ Get list of sandbox activity 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') @@ -254,7 +266,7 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from 'error_only' - to filter for error events only """ self._validate_auth_headers() - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity" params = {} if error_only: @@ -278,7 +290,7 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from def get_sandbox_commands(self, sandbox_id: str): """ Get list of sandbox commands """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'")) return response.json() @@ -286,18 +298,20 @@ def get_sandbox_commands(self, sandbox_id: str): def get_sandbox_command_details(self, sandbox_id: str, command_name: str): """ Get details of specific sandbox command """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}', - headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers + ) if not response.ok: - err_msg = self._format_err(response, - f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'") + err_msg = self._format_err( + response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'" + ) raise SandboxRestException(err_msg) return response.json() def get_sandbox_components(self, sandbox_id: str): """ Get list of sandbox components """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers) if not response.ok: err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'") raise SandboxRestException(err_msg) @@ -306,11 +320,13 @@ def get_sandbox_components(self, sandbox_id: str): def get_sandbox_component_details(self, sandbox_id: str, component_id: str): """ Get details of components in sandbox """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}', - headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers + ) if not response.ok: - custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', " - f"sandbox: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + ) err_msg = self._format_err(response, custom_err_msg) raise SandboxRestException(err_msg) return response.json() @@ -318,11 +334,13 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str): def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): """ Get list of commands for a particular component in sandbox """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands', - headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers + ) if not response.ok: - custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', " - f"sandbox: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + ) err_msg = self._format_err(response, custom_err_msg) raise SandboxRestException(err_msg) return response.json() @@ -331,11 +349,13 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s """ Get details of a command of sandbox component """ self._validate_auth_headers() response = requests.get( - f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}', - headers=self._auth_headers) + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}", + headers=self._auth_headers, + ) if not response.ok: - custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', " - f"sandbox: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + ) err_msg = self._format_err(response, custom_err_msg) raise SandboxRestException(err_msg) return response.json() @@ -343,18 +363,16 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s def get_sandbox_instructions(self, sandbox_id: str): """ pull the instruction text of sandbox """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions', - headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, - f"Failed to get sandbox instructions for '{sandbox_id}'") + err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") raise SandboxRestException(err_msg) return response.json() def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None): """ Get list of sandbox output """ self._validate_auth_headers() - url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions' + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions" params = {} if tail: params["tail"] = tail @@ -369,8 +387,7 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i response = requests.get(url, headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, - f"Failed to get sandbox instructions for '{sandbox_id}'") + err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") raise SandboxRestException(err_msg) return response.json() @@ -378,7 +395,7 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i def get_blueprints(self): """ Get list of blueprints """ self._validate_auth_headers() - response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) if not response.ok: raise SandboxRestException(self._format_err(response, "Failed to get blueprints")) return response.json() @@ -389,10 +406,9 @@ def get_blueprint_details(self, blueprint_id: str): Can pass either blueprint name OR blueprint ID """ self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'", headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/blueprints/{blueprint_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) + raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) return response.json() # EXECUTIONS @@ -400,35 +416,28 @@ def get_execution_details(self, execution_id: str): self._validate_auth_headers() response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"Failed to get execution details for '{execution_id}'")) + raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'")) return response.json() def delete_execution(self, execution_id: str): self._validate_auth_headers() response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - self._format_err(response, f"Failed to delete execution for '{execution_id}'")) + raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'")) return response.json() -if __name__ == '__main__': - API_SERVER = "localhost" - ADMIN_USER = "admin" - ADMIN_PASS = "admin" - - admin_api = SandboxRestApiSession(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS) - with admin_api: - # sample api call with admin user session to get all sandboxes - # all_sandboxes = admin_api.get_sandboxes() - # print("== List of sandboxes pulled by Admin ===") - # print(json.dumps(all_sandboxes, indent=4)) - - sb_res = admin_api.start_sandbox(blueprint_id="rest test", sandbox_name="rest test") - print(sb_res["state"]) - time.sleep(4) - +if __name__ == "__main__": + TARGET_END_USER = "domain_admin" + TARGET_END_USER = "natti" + admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin") + with admin_api: + all_sandboxes = admin_api.get_sandboxes() + print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}") + end_user_token = admin_api.get_token_for_target_user(TARGET_END_USER) + with SandboxRestApiSession(host="localhost", username=TARGET_END_USER, token=end_user_token) as user_rest: + user_bps = user_rest.get_blueprints() + print(f"Blueprint count pulled by user '{TARGET_END_USER}': {len(user_bps)}") diff --git a/cloudshell/sandbox_rest/sandbox_components.py b/cloudshell/sandbox_rest/sandbox_components.py index 6fd9208..7325eab 100644 --- a/cloudshell/sandbox_rest/sandbox_components.py +++ b/cloudshell/sandbox_rest/sandbox_components.py @@ -1,6 +1,6 @@ import json -from typing import List from enum import Enum +from typing import List class ComponentTypes(Enum): @@ -26,7 +26,7 @@ def __init__(self, components: List[dict] = None): """ if instantiated with components then populate lists if not, then call "refresh_components" after getting data from sandbox - """ + """ self.all_components = components if components else [] self.services = [] self.resources = [] @@ -40,9 +40,11 @@ def _filter_components_by_type(self, component_type: str) -> List[dict]: return [component for component in self.all_components if component["type"] == component_type] def _filter_app_by_lifecycle(self, lifecycle_type): - return [component for component in self.all_components - if component["type"] == ComponentTypes.app_type.value - and component["app_lifecyle"] == lifecycle_type] + return [ + component + for component in self.all_components + if component["type"] == ComponentTypes.app_type.value and component["app_lifecyle"] == lifecycle_type + ] def _sort_components(self) -> None: """ sort stored components into separate lists """ @@ -94,5 +96,7 @@ def _validate_components_for_attributes(components: List[dict]): return attrs = components[0].get("attributes") if not attrs: - raise Exception("'attributes' member not found. Must pass in Full info components.\n" - f"components data passed:\n{json.dumps(components, indent=4)}") + raise Exception( + "'attributes' member not found. Must pass in Full info components.\n" + f"components data passed:\n{json.dumps(components, indent=4)}" + ) diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py index 93f4b64..473c7c5 100644 --- a/cloudshell/sandbox_rest/sandbox_controller.py +++ b/cloudshell/sandbox_rest/sandbox_controller.py @@ -1,19 +1,28 @@ -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam -from cloudshell.sandbox_rest.helpers.polling_helpers import poll_sandbox_setup, poll_sandbox_teardown, \ - poll_execution_for_completion, SandboxStates, SANDBOX_SETUP_STATES, SANDBOX_ACTIVE_STATES -from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents import json -from typing import List from timeit import default_timer +from typing import List + +from cloudshell.sandbox_rest.helpers.polling_helpers import ( + SANDBOX_ACTIVE_STATES, + SANDBOX_SETUP_STATES, + SandboxStates, + poll_execution_for_completion, + poll_sandbox_setup, + poll_sandbox_teardown, +) +from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession +from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents class SandboxSetupError(Exception): """ When sandbox has error during setup """ + pass class SandboxTeardownError(Exception): """ When sandbox has error during setup """ + pass @@ -64,20 +73,27 @@ def _print(self, message): if not self.disable_prints: print(message) - def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M", - bp_params: List[InputParam] = None, permitted_users: List[str] = None, - max_polling_minutes=30, raise_setup_exception=True) -> dict: + def start_sandbox_and_poll( + self, + blueprint_id: str, + sandbox_name="", + duration="PT1H30M", + bp_params: List[InputParam] = None, + permitted_users: List[str] = None, + max_polling_minutes=30, + raise_setup_exception=True, + ) -> dict: """ Start sandbox, poll for result, get back sandbox info """ if self.sandbox_id: - raise ValueError(f"Sandbox already has id '{self.sandbox_id}'.\n" - f"Start blueprint with new sandbox controller instance") + raise ValueError( + f"Sandbox already has id '{self.sandbox_id}'.\n" f"Start blueprint with new sandbox controller instance" + ) self._print(f"Starting blueprint {blueprint_id}") start = default_timer() start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users) self.sandbox_id = start_response["id"] - sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes, - polling_frequency_seconds=30) + sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes, polling_frequency_seconds=30) self.update_components() sandbox_state = sandbox_details["state"] stage = sandbox_details["setup_stage"] @@ -97,8 +113,10 @@ def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="P self._print(err_msg) if raise_setup_exception: - err_msg = (f"Sandbox '{name}' Error during SETUP. See events for details.\n" - f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'") + err_msg = ( + f"Sandbox '{name}' Error during SETUP. See events for details.\n" + f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'" + ) raise SandboxSetupError(err_msg) total_minutes = (default_timer() - start) / 60 @@ -112,8 +130,7 @@ def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_t last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"] start = default_timer() self.api.stop_sandbox(sandbox_id) - sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, - polling_frequency_seconds=30) + sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, polling_frequency_seconds=30) self.update_components() sandbox_state = sandbox_details["state"] stage = sandbox_details["setup_stage"] @@ -123,18 +140,19 @@ def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_t self.sandbox_ended = True tail_error_count = 5 - tailed_errors = self.api.get_sandbox_activity(sandbox_id, - error_only=True, - from_event_id=last_activity_id + 1, - tail=tail_error_count)["events"] + tailed_errors = self.api.get_sandbox_activity( + sandbox_id, error_only=True, from_event_id=last_activity_id + 1, tail=tail_error_count + )["events"] if tailed_errors: self.sandbox_ended = True self.teardown_errors = tailed_errors self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}") if raise_teardown_exception: - err_msg = (f"Sandbox '{name}' Error during SETUP.\n" - f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'") + err_msg = ( + f"Sandbox '{name}' Error during SETUP.\n" + f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'" + ) raise SandboxTeardownError(err_msg) total_minutes = (default_timer() - start) / 60 @@ -142,6 +160,7 @@ def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_t return sandbox_details def rerun_setup(self): + self pass def run_sandbox_command_and_poll(self): diff --git a/setup.py b/setup.py index a6d1d5e..cccdeec 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ -from setuptools import setup, find_packages from typing import List +from setuptools import find_packages, setup + + def read_file(file_name: str) -> str: with open(file_name) as f: content = f.read().strip() @@ -14,22 +16,22 @@ def lines_from_file(file_name: str) -> List[str]: setup( - name='cloudshell_sandboxapi_wrapper', - description='Python client for CloudShell Sandbox REST api - consume sandboxes via CI', + name="cloudshell_sandboxapi_wrapper", + description="Python client for CloudShell Sandbox REST api - consume sandboxes via CI", keywords=["cloudshell", "sandbox", "api", "CI"], - url='https://github.com/QualiSystemsLab/Sandbox-API-Python', - author='sadanand.s', - author_email='sadanand.s@quali.com', - license='Apache 2.0', + url="https://github.com/QualiSystemsLab/Sandbox-API-Python", + author="sadanand.s", + author_email="sadanand.s@quali.com", + license="Apache 2.0", packages=find_packages(), version=read_file("version.txt"), long_description=read_file("README.rst"), install_requires=lines_from_file("requirements.txt"), test_requires=lines_from_file("test-requirements.txt"), classifiers=[ - 'License :: OSI Approved :: Apache Software License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ] -) \ No newline at end of file + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ], +) diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000..8539c15 --- /dev/null +++ b/tests/common.py @@ -0,0 +1,10 @@ +import json + +DUT_RESOURCE = "DUT_1" +DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" +HEALTH_CHECK_COMMAND = "health_check" + + +def pretty_print_response(dict_response): + json_str = json.dumps(dict_response, indent=4) + print(f"\n{json_str}") diff --git a/tests/common_fixtures.py b/tests/common_fixtures.py new file mode 100644 index 0000000..5dcb85f --- /dev/null +++ b/tests/common_fixtures.py @@ -0,0 +1,20 @@ +import pytest + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession + + +@pytest.fixture +def dut_bp_name() -> str: + """ For testing basic operations """ + return "DUT Test" + + +@pytest.fixture +def dut_resource_name() -> str: + """ For testing basic operations """ + return "DUT_1" + + +@pytest.fixture +def admin_session() -> SandboxRestApiSession: + return SandboxRestApiSession("localhost", "admin", "admin") diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py new file mode 100644 index 0000000..6ea9528 --- /dev/null +++ b/tests/test_api _no_sandbox.py @@ -0,0 +1,35 @@ +""" +Test the api methods that do not require a live sandbox +- get all blueprints +- get all sandboxes +- get blueprint by id +- get token + delete token +""" +from common import DEFAULT_BLUEPRINT_TEMPLATE, pretty_print_response +from common_fixtures import admin_session + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession + + +def test_get_sandboxes(admin_session: SandboxRestApiSession): + res = admin_session.get_sandboxes() + print(f"Sandbox count found in system: {len(res)}") + + +def test_get_blueprints(admin_session: SandboxRestApiSession): + bp_res = admin_session.get_blueprints() + print(f"Blueprint count found in system: '{len(bp_res)}'") + + +def test_get_default_blueprint(admin_session: SandboxRestApiSession): + bp_res = admin_session.get_blueprint_details(DEFAULT_BLUEPRINT_TEMPLATE) + bp_name = bp_res["name"] + print(f"Pulled details for '{bp_name}'") + + +def test_get_and_delete_token(admin_session: SandboxRestApiSession): + """ get token for admin user """ + token_res = admin_session.get_token_for_target_user("admin") + print(f"Token response: '{token_res}'") + admin_session.delete_token(token_res) + print("deleted token") diff --git a/tests/test_api_with_sandbox.py b/tests/test_api_with_sandbox.py new file mode 100644 index 0000000..8c1f97a --- /dev/null +++ b/tests/test_api_with_sandbox.py @@ -0,0 +1,33 @@ +""" +Test the api methods that require a live sandbox against default +- start sandbox +""" +from common import DEFAULT_BLUEPRINT_TEMPLATE +from common_fixtures import admin_session + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession + + +def test_simple_flow(admin_session: SandboxRestApiSession): + """ """ + # start sandbox + start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest simple flow") + sandbox_id = start_res["id"] + assert sandbox_id + print(f"Sandbox started. ID: {sandbox_id}") + + # get sandbox details + details_res = admin_session.get_sandbox_details(sandbox_id) + sb_name = details_res["name"] + print(f"Pulled details for sandbox '{sb_name}'") + + # get components - should be empty + sb_components = admin_session.get_sandbox_components(sandbox_id) + component_count = len(sb_components) + print(f"component count found: {component_count}") + + # get activity events + + # get commands + + # diff --git a/tests/test_rest_client.py b/tests/test_rest_client.py deleted file mode 100644 index 0e5af88..0000000 --- a/tests/test_rest_client.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest -from cloudshell_test.sandbox_rest.sandbox_api import SandboxRestApiClient -from datetime import datetime -import json -from dataclasses import dataclass - -DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" -DUT_RESOURCE = "DUT_1" -HEALTH_CHECK_COMMAND = "health_check" - - -@dataclass -class SandboxTestData: - blueprint_name: str - sandbox_command: str - - -@dataclass -class ResourceTestData: - resource_name: str - resource_command: str - - -@pytest.fixture(name="dut_bp") -def dut_blueprint_fixture(): - """ For testing basic operations """ - return "DUT Test" - - -@pytest.fixture(name="session") -def rest_session_fixture(): - return SandboxRestApiClient("localhost", "admin", "admin") - - -def _pretty_print_response(dict_response): - json_str = json.dumps(dict_response, indent=4) - print(f"\n{json_str}") - - -class TestNoBlueprint: - def test_get_sandboxes(self, session: SandboxRestApiClient): - res = session.get_sandboxes() - _pretty_print_response(res) - - -class TestEmptyBlueprint: - bp_data = SandboxTestData(blueprint_name=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_command="Setup") - - def test_simple_flow(self, session: SandboxRestApiClient): - start_response = session.start_sandbox(self.bp_data.blueprint_name, f"test sandbox - {datetime.now()}") - print(f"\nblueprint {start_response['name']} started") diff --git a/tests/test_sandbox_controller.py b/tests/test_sandbox_controller.py new file mode 100644 index 0000000..e69de29 From d5c65f552933e70c6c5286257ecab8763d5f957b Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Sun, 24 Oct 2021 14:28:53 +0300 Subject: [PATCH 08/29] changed init files to remove author --- build/lib/cloudshell/__init__.py | 4 - build/lib/cloudshell/sandbox_rest/__init__.py | 4 - .../cloudshell/sandbox_rest/flow_helpers.py | 0 .../cloudshell/sandbox_rest/sandbox_api.py | 331 ------------------ cloudshell/__init__.py | 2 - cloudshell/sandbox_rest/__init__.py | 2 - .../__pycache__/__init__.cpython-37.pyc | Bin 283 -> 283 bytes .../__pycache__/sandbox_api.cpython-37.pyc | Bin 17230 -> 17475 bytes cloudshell/sandbox_rest/helpers/__init__.py | 3 + cloudshell/sandbox_rest/sandbox_api.py | 10 +- cloudshell/sandbox_rest/sandbox_controller.py | 2 +- .../SOURCES.txt | 5 +- .../requires.txt | 1 + 13 files changed, 14 insertions(+), 350 deletions(-) delete mode 100644 build/lib/cloudshell/__init__.py delete mode 100644 build/lib/cloudshell/sandbox_rest/__init__.py delete mode 100644 build/lib/cloudshell/sandbox_rest/flow_helpers.py delete mode 100644 build/lib/cloudshell/sandbox_rest/sandbox_api.py diff --git a/build/lib/cloudshell/__init__.py b/build/lib/cloudshell/__init__.py deleted file mode 100644 index cee05d2..0000000 --- a/build/lib/cloudshell/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__author__ = "quali" -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) diff --git a/build/lib/cloudshell/sandbox_rest/__init__.py b/build/lib/cloudshell/sandbox_rest/__init__.py deleted file mode 100644 index cee05d2..0000000 --- a/build/lib/cloudshell/sandbox_rest/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__author__ = "quali" -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) diff --git a/build/lib/cloudshell/sandbox_rest/flow_helpers.py b/build/lib/cloudshell/sandbox_rest/flow_helpers.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/cloudshell/sandbox_rest/sandbox_api.py b/build/lib/cloudshell/sandbox_rest/sandbox_api.py deleted file mode 100644 index 6b1bf2b..0000000 --- a/build/lib/cloudshell/sandbox_rest/sandbox_api.py +++ /dev/null @@ -1,331 +0,0 @@ -import json -from dataclasses import asdict, dataclass -from typing import List - -import requests - - -class SandboxRestException(Exception): - """ General exception to raise inside Rest client class """ - - pass - - -@dataclass -class InputParam: - """ To model the param objects passed to sandbox / component command endpoints """ - - name: str - value: str - - -class SandboxRestApiClient: - """ - Python wrapper for CloudShell Sandbox API - View http:///api/v2/explore to see schemas of return json values - """ - - def __init__(self, host: str, username: str, password: str, domain="Global", port=82, is_https=False, api_version="v2"): - """ login to api and store headers for future requests """ - protocol = "https" if is_https else "http" - self._base_url = f"{protocol}://{host}:{port}/api" - self._versioned_url = f"{self._base_url}/{api_version}" - self._auth_headers = self._get_auth_headers(username, password, domain) - - def _get_auth_headers(self, user_name: str, password: str, domain: str): - """ - Get token from login response, then place token into auth headers on class - """ - login_res = requests.put( - url=f"{self._base_url}/login", - data=json.dumps({"username": user_name, "password": password, "domain": domain}), - headers={"Content-Type": "application/json"}, - ) - - if not login_res.ok: - raise SandboxRestException(self._format_err(login_res, "Failed Login")) - - login_token = login_res.text[1:-1] - - auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"} - return auth_headers - - @staticmethod - def _format_err(response: requests.Response, custom_err_msg="Failed Api Call"): - err_msg = f"Response Code: {response.status_code}, Reason: {response.reason}" - if custom_err_msg: - err_msg = f"{custom_err_msg}. {err_msg}" - return err_msg - - # SANDBOX POST REQUESTS - def start_sandbox( - self, - blueprint_id: str, - sandbox_name="", - duration="PT2H0M", - bp_params: List[InputParam] = None, - permitted_users: List[str] = None, - ): - """ - Create a sandbox from the provided blueprint id - Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') - """ - url = f"{self._versioned_url}/blueprints/{blueprint_id}/start" - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] - - data_dict = { - "duration": duration, - "name": sandbox_name, - "permitted_users": permitted_users if permitted_users else [], - "params": [asdict(x) for x in bp_params] if bp_params else [], - } - - response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) - if not response.ok: - err_msg = self._format_err(response, f"Failed to start sandbox from blueprint '{blueprint_id}'") - raise SandboxRestException(err_msg) - return response.json() - - def start_persistent_sandbox( - self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None - ): - """ Create a persistent sandbox from the provided blueprint id """ - url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" - - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] - data_dict = { - "name": sandbox_name, - "permitted_users": permitted_users if permitted_users else [], - "params": [asdict(x) for x in bp_params] if bp_params else [], - } - - response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) - if not response.ok: - err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'") - raise SandboxRestException(err_msg) - return response.json() - - def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True): - """ Run a sandbox level command """ - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" - data_dict = {"printOutput": print_output} - params = [asdict(x) for x in params] if params else [] - data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'")) - return response.json() - - def run_component_command( - self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True - ): - """ Start a command on sandbox component """ - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" - data_dict = {"printOutput": print_output} - params = [asdict(x) for x in params] if params else [] - data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'")) - return response.json() - - def extend_sandbox(self, sandbox_id: str, duration: str): - """Extend the sandbox - :param str sandbox_id: Sandbox id - :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) - :return: - """ - data_dict = {"extended_time": duration} - response = requests.post( - f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers - ) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'")) - return response.json() - - def stop_sandbox(self, sandbox_id: str): - """ Stop the sandbox given sandbox id """ - response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) - return response.json() - - # SANDBOX GET REQUESTS - def get_sandboxes(self, show_historic=False): - """Get list of sandboxes - :return: - """ - params = {"show_historic": show_historic} - response = requests.get(f"{self._versioned_url}/sandboxes", headers=self._auth_headers, params=params) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list")) - return response.json() - - def get_sandbox_details(self, sandbox_id: str): - """ Get details of the given sandbox id """ - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'")) - return response.json() - - def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None): - """ - Get list of sandbox activity - 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') - 'from_event_id' - integer id of event where to start pulling results from - 'tail' - how many of the last entries you want to pull - 'error_only' - to filter for error events only - """ - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity" - params = {} - - if error_only: - params["error_only"] = error_only - if since: - params["since"] = since - if from_event_id: - params["from_event_id"] = from_event_id - if tail: - params["tail"] = tail - - if params: - response = requests.get(url, headers=self._auth_headers, params=params) - else: - response = requests.get(url, headers=self._auth_headers) - - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'")) - return response.json() - - def get_sandbox_commands(self, sandbox_id: str): - """ Get list of sandbox commands """ - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'")) - return response.json() - - def get_sandbox_command_details(self, sandbox_id: str, command_name: str): - """ Get details of specific sandbox command """ - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers - ) - if not response.ok: - err_msg = self._format_err( - response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'" - ) - raise SandboxRestException(err_msg) - return response.json() - - def get_sandbox_components(self, sandbox_id: str): - """ Get list of sandbox components """ - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers) - if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'") - raise SandboxRestException(err_msg) - return response.json() - - def get_sandbox_component_details(self, sandbox_id: str, component_id: str): - """ Get details of components in sandbox """ - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers - ) - if not response.ok: - custom_err_msg = ( - f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" - ) - err_msg = self._format_err(response, custom_err_msg) - raise SandboxRestException(err_msg) - return response.json() - - def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): - """ Get list of commands for a particular component in sandbox """ - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers - ) - if not response.ok: - custom_err_msg = ( - f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'" - ) - err_msg = self._format_err(response, custom_err_msg) - raise SandboxRestException(err_msg) - return response.json() - - def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str): - """ Get details of a command of sandbox component """ - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}", - headers=self._auth_headers, - ) - if not response.ok: - custom_err_msg = ( - f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" - ) - err_msg = self._format_err(response, custom_err_msg) - raise SandboxRestException(err_msg) - return response.json() - - def get_sandbox_instructions(self, sandbox_id: str): - """ pull the instruction text of sandbox """ - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers) - if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") - raise SandboxRestException(err_msg) - return response.json() - - def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None): - """ Get list of sandbox output """ - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions" - params = {} - if tail: - params["tail"] = tail - if from_entry_id: - params["from_entry_id"] = from_entry_id - if since: - params["since"] = since - - if params: - response = requests.get(url, headers=self._auth_headers, params=params) - else: - response = requests.get(url, headers=self._auth_headers) - - if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") - raise SandboxRestException(err_msg) - return response.json() - - # BLUEPRINT GET REQUESTS - def get_blueprints(self): - """ Get list of blueprints """ - response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, "Failed to get blueprints")) - return response.json() - - def get_blueprint_details(self, blueprint_id: str): - """ - Get details of a specific blueprint - Can pass either blueprint name OR blueprint ID - """ - response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'") - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) - return response.json() - - # EXECUTIONS - def get_execution_details(self, execution_id: str): - response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'")) - return response.json() - - def delete_execution(self, execution_id: str): - response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'")) - return response.json() - - -if __name__ == "__main__": - api = SandboxRestApiClient("localhost", "admin", "admin") - bps = api.get_blueprints() - pass diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py index cee05d2..3ad9513 100644 --- a/cloudshell/__init__.py +++ b/cloudshell/__init__.py @@ -1,4 +1,2 @@ -__author__ = "quali" from pkgutil import extend_path - __path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py index cee05d2..3ad9513 100644 --- a/cloudshell/sandbox_rest/__init__.py +++ b/cloudshell/sandbox_rest/__init__.py @@ -1,4 +1,2 @@ -__author__ = "quali" from pkgutil import extend_path - __path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc index 16c0d26a19c161f66f05ab525034462bea0f8f07..eee6736ad4ff2f82925fd57e10453058b115b48f 100644 GIT binary patch delta 48 zcmbQuG@FUniI~oVB&3iMkW9t C<_*07 delta 48 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 AKmY&$ diff --git a/cloudshell/sandbox_rest/helpers/__init__.py b/cloudshell/sandbox_rest/helpers/__init__.py index e69de29..b36383a 100644 --- a/cloudshell/sandbox_rest/helpers/__init__.py +++ b/cloudshell/sandbox_rest/helpers/__init__.py @@ -0,0 +1,3 @@ +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index ae4f970..2b15a88 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -1,8 +1,6 @@ import json -import time from dataclasses import asdict, dataclass from typing import List - import requests @@ -428,11 +426,13 @@ def delete_execution(self, execution_id: str): if __name__ == "__main__": - TARGET_END_USER = "domain_admin" TARGET_END_USER = "natti" + TARGET_DOMAIN = "end_users" + TARGET_DOMAIN = "Global" - admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin") - + # admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN) + admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN) + from cloudshell.api.cloudshell_api import CloudShellAPISession with admin_api: all_sandboxes = admin_api.get_sandboxes() print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}") diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py index 473c7c5..46fae1a 100644 --- a/cloudshell/sandbox_rest/sandbox_controller.py +++ b/cloudshell/sandbox_rest/sandbox_controller.py @@ -12,7 +12,7 @@ ) from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents - +from cloudshell.logger.qs_logger import get_qs_logger class SandboxSetupError(Exception): """ When sandbox has error during setup """ diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt index e9351d7..1998237 100644 --- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt +++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt @@ -2,8 +2,11 @@ README.rst setup.py cloudshell/__init__.py cloudshell/sandbox_rest/__init__.py -cloudshell/sandbox_rest/flow_helpers.py cloudshell/sandbox_rest/sandbox_api.py +cloudshell/sandbox_rest/sandbox_components.py +cloudshell/sandbox_rest/sandbox_controller.py +cloudshell/sandbox_rest/helpers/__init__.py +cloudshell/sandbox_rest/helpers/polling_helpers.py cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt diff --git a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt index f229360..c3e8e97 100644 --- a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt +++ b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt @@ -1 +1,2 @@ requests +retrying From 7dd06cd9de621421034123b22cee553ed39d7a43 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Sun, 24 Oct 2021 14:29:22 +0300 Subject: [PATCH 09/29] testing init changes --- cloudshell/__init__.py | 1 + cloudshell/sandbox_rest/__init__.py | 1 + cloudshell/sandbox_rest/sandbox_api.py | 2 ++ cloudshell/sandbox_rest/sandbox_controller.py | 3 ++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py index 3ad9513..b36383a 100644 --- a/cloudshell/__init__.py +++ b/cloudshell/__init__.py @@ -1,2 +1,3 @@ from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py index 3ad9513..b36383a 100644 --- a/cloudshell/sandbox_rest/__init__.py +++ b/cloudshell/sandbox_rest/__init__.py @@ -1,2 +1,3 @@ from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 2b15a88..97b090f 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -1,6 +1,7 @@ import json from dataclasses import asdict, dataclass from typing import List + import requests @@ -433,6 +434,7 @@ def delete_execution(self, execution_id: str): # admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN) admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN) from cloudshell.api.cloudshell_api import CloudShellAPISession + with admin_api: all_sandboxes = admin_api.get_sandboxes() print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}") diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py index 46fae1a..bc79418 100644 --- a/cloudshell/sandbox_rest/sandbox_controller.py +++ b/cloudshell/sandbox_rest/sandbox_controller.py @@ -2,6 +2,7 @@ from timeit import default_timer from typing import List +from cloudshell.logger.qs_logger import get_qs_logger from cloudshell.sandbox_rest.helpers.polling_helpers import ( SANDBOX_ACTIVE_STATES, SANDBOX_SETUP_STATES, @@ -12,7 +13,7 @@ ) from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents -from cloudshell.logger.qs_logger import get_qs_logger + class SandboxSetupError(Exception): """ When sandbox has error during setup """ From c67b632e2d20308df568c3568319bd93675d449f Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Tue, 26 Oct 2021 02:10:38 +0300 Subject: [PATCH 10/29] refactored exception formatting and added tests --- .gitignore | 3 +- .../sandbox_components.cpython-37.pyc | Bin 5053 -> 0 bytes cloudshell/sandbox_rest/exceptions.py | 28 +++ cloudshell/sandbox_rest/helpers/__init__.py | 3 - .../__pycache__/__init__.cpython-37.pyc | Bin 191 -> 0 bytes .../polling_helpers.cpython-37.pyc | Bin 4240 -> 0 bytes .../sandbox_rest/helpers/polling_helpers.py | 131 ---------- cloudshell/sandbox_rest/sandbox_api.py | 228 +++++++----------- cloudshell/sandbox_rest/sandbox_components.py | 102 -------- cloudshell/sandbox_rest/sandbox_controller.py | 185 -------------- .../PKG-INFO | 58 ++--- .../SOURCES.txt | 6 +- test-requirements.txt | 3 +- tests/common.py | 4 - tests/common_fixtures.py | 20 -- tests/conftest.py | 14 ++ tests/env_settings.py | 22 ++ tests/test_api _no_sandbox.py | 6 +- tests/test_api_empty_sandbox.py | 67 +++++ tests/test_api_with_sandbox.py | 33 --- tests/test_sandbox_controller.py | 0 21 files changed, 262 insertions(+), 651 deletions(-) delete mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc create mode 100644 cloudshell/sandbox_rest/exceptions.py delete mode 100644 cloudshell/sandbox_rest/helpers/__init__.py delete mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/helpers/polling_helpers.py delete mode 100644 cloudshell/sandbox_rest/sandbox_components.py delete mode 100644 cloudshell/sandbox_rest/sandbox_controller.py delete mode 100644 tests/common_fixtures.py create mode 100644 tests/conftest.py create mode 100644 tests/env_settings.py create mode 100644 tests/test_api_empty_sandbox.py delete mode 100644 tests/test_api_with_sandbox.py delete mode 100644 tests/test_sandbox_controller.py diff --git a/.gitignore b/.gitignore index 8750fc0..25cf4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ .pytest_cache/ -*.egg \ No newline at end of file +*.egg +tests/.env \ No newline at end of file diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc deleted file mode 100644 index 1840cfc278748ee38bd9e3c24ab13b2c07d34784..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5053 zcmb_g&u`qu73S|$a(3xVB{{D3A(i-P8c9fTcLI78NeJ zn&H~gmcCR5+Di{T^i&71j{Ph8Zxp=tlz$<;^m{`tclE=K4U`Mc&Ld~u%)D>j_ulZ{ z++0h;we-*52md*tY5yi+d|9a6!7VpXFpcT4=Fz_q>wUvBbgG-N*|$7P*FMvj#q8%A zv$?frdNyQ-H6S;X>_Bd^8OSqAZa{9aS;(_{hSy~JL#^BX7UO7ZUA?eAk7P#w50YU& zq^ILcN97J~c@_oNJe_Ht!F11LhG#L1%PeTJi_U7=AEXISvfu0uxb$Z44hC@)23eFQ zMf2BOrb7{OuknzJCsD}Px=zvb{Uqpf-!EFe-%r^vrh41=w}(Moy=ex6fuBKRF()b= zrP!9GA|?Gp6KzaS72j>G-dz6@Z{>OtWLdQG^?H~xzD`P`^?oEncOcR&9%gd=VUVzm zbm!{bFMe_Li`{H9P1eIW9WuGe<9J<`Z+#4$)j~L4rd%293PQ{w>rm0uE&SB8<4w4L zMn_Eu^I647nCtVX$5(g5n3oo8-sgij-Q~<{eVMSTzSgw`jU^gXG*J}I@w;MnnD}F1 zVvw>nN9{W($WW5zP199Rj-Tv#G&wq1!rOD0jY&I2=WdpXXk(aFGj438Y0LxQJqV7&N)mq@t9{`SnO?f7Ck%l zyK;t$=(ohq;mwJ$PBrWE>>Z_RUb*+X#p!B;!~L?_KP5SQYqF0+14f{W{^?%DEk8k# z>!O+KiSby=HD6PuuFCR>sp?j)ZE500uD!JPHBb~0i!u6J%dM`c=SGRlf+UNAj5GIX zlx@0`(cNs5C+;8}3}d`@WAKoi?wh39m9kA3#Id_9cn|)#=}$B)yFoAG!hOu)?a5=8 z1zF(sMA~;N2d>q*#5x_Qnd)6MDNTww()a;J#)W6Y%V}V*3cZ2dyy?jpt1Q=)Sf`z?h@!ZV-likV$tV1rTx*%K$*VbQ%*7pW7S4+~o_30d&#k zAm(@rwvlHzygewYsE~xh^{W)g@Oq; zeb>50*i(bsmq|Yj=4hk)udqxniUIl*0RG0Vvdqe@IK@_xw^11@#At6rUDpur(4h6m z<5?(>mi$-h{9rsG%@|#K(+RyPL~G%(_P&BSD0{y*ws)p&>03r=bEY4$zaf5rE?%1= zXpGPj?#8@ps_-Oc@TRbz1o4o=D9ezmWc7os#WJb|5#h=GIIlw<>wIlG_y(UU7*kT$!Z;^jCX;gmY!7EBPQ0 zHS>X?=EVCLyM&2SMM0ZS5OIiEDku*V@LF11-@Hmn@ zb(GqFf-Wjo(Lj}>%g0akXt#rONttAcwAv?7NhsGy)3)BZP;w-=%*c`5E@CC{t7dLI z)V7_eAA>(tIng)4^}Dj3(z?@_ow zBBAnl=~9TeJ4x}S8$#Vsas=Hz3QC~ek6bm)T1D+@G}Si1K&3|+!Z_M(dte;c=Qy5FKgw3O@nx{mPh%-Xq^>-V*x=6^} z&EN@lL0(Z3#zV$Sm;rY$=tuFcYAgpq$lanX!k@jKxpPSf!eA-h+q%nSl_J?4&Krnp#eJ~u+bbwz70OcbzpBzz-NIb{+ z1O~+!Z(pZ0N?y-!*2@s$@dlq${$l)H`^MPQ|A>SZhIQ5iWd+JKBkvMTSsi!fCAZJ} z8we}p4ZU=juod^0Lz&ft`spNlo_sKvI&_t%6rx|wZnnN9=SK47AY$MP3cd%JIL#C@ z4|XV3r!OehmPBc@VSgYIM9BdPo8}`AnOfoXJG_?aJCi}5Bs#c9uOP$M@I#8>Cl8(4 z1yOimu_(;M1*)1jsF#_G%1tQPi!K$Ew-#+20JA9Ub9`fAVuhanlnM%l5m zD^8=JbVq%AYU&Gf3s%$aG#0FR+tAN9&bO9YhOSz5X9V$Y;o!u9Ghj74j_(D69J0)& k=-j2g1*k4lcB@A$nzzdDP`3$oNza=UrOkO~-kxv#7tadp?f?J) diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py new file mode 100644 index 0000000..634586d --- /dev/null +++ b/cloudshell/sandbox_rest/exceptions.py @@ -0,0 +1,28 @@ +from requests import Response + + +class SandboxRestException(Exception): + """ Base Exception Class inside Rest client class """ + def __init__(self, msg: str, response: Response = None): + self.msg = msg + self.response = response + + def __str__(self): + return self._format_err_msg(self.msg, self.response) + + def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str: + err_msg = f"Sandbox API Error: {custom_err_msg}" + + if response: + err_msg += f"\n{self._format_response_msg(response)}" + + @staticmethod + def _format_response_msg(response: Response): + return (f"Response: {response.status_code}, Reason: {response.reason}\n" + f"Request URL: {response.request.url}\n" + f"Request Headers: {response.request.headers}") + + +class SandboxRestAuthException(SandboxRestException): + """ Failed auth action """ + pass diff --git a/cloudshell/sandbox_rest/helpers/__init__.py b/cloudshell/sandbox_rest/helpers/__init__.py deleted file mode 100644 index b36383a..0000000 --- a/cloudshell/sandbox_rest/helpers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 2b20b3f99c96cfe3923b9169a2ea40bf89947b6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 191 zcmZ?b<>g`kf+;8C5<&E15CH>>K!yVl7qb9~6oz01O-8?!3`HPe1o11w*(xTqIJKxa zCNHt1BvUUtCOJPPHKwpMF()%7H?ufdp`a)~D>b>KI3_qTFC{6zLf0|CQ#YWpBqKjB zCOId+G^IErH76&g7$O^Alv-R80~9F$nHe9SnU`4-AFo$Xd5gm)H$SB`C)Ez*g3mzA F007H&HckKl diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc deleted file mode 100644 index 18e0a2b4aa9cb546cee1bcb3c1a4cdae242d1c9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4240 zcmb7HTXWmS6~^L1Qj|o|y4aR%$&jyQ6U*s!rk*rYTcH!pbY#VpTo=PB3Sn0gBoJU0 zKrCsbm&!BoOJDm^ccyvCO#TvH`%?EO^eNw2kQAjzofMp1?%7?OJ=gCno{o=KH9TK@ z>v?~>qG|skqW_dJ_#ImEOQvaDb2QGl9|R&&te6uwcD(O9SNJ68c~KDy!VtIc zTi{dgN_?8nVBY3;)yf>d{?6dDeD00qEb@Cpb2s=+%-vS}=D~k~-@AFiyZlZ|Ydkpow)(jUyF%JqS>&fd93^%d+ub+}?Q}=jN$RD-j<@W@i}+@|Zzm$n zx^^tps@NBPMlY#-Swb5<+mAaP>|UyFBtf)oKae7o2N*rD!(dZ-@_KLqppm1g`Ym^zxk;8o_yZWbMyB>lIE3VFATlSP+-PVoAT-lI;AOP zEE_DJX%*tW5J~!^8?;1{KuRo_%_!^m#H{}ybc!Gv!$4>bLm=s#!8m$;UABM~xb zM_!r+OM7iU=AuozgLWrKe7mQ4((Wnn@X6ZB!?lBSCyv^F7-u}$5n?{DIzbgfybOl%v!Q6f8#LG*?4cN9&(V z^u;IN#mcGO9fRwR$qmQirc>gUQ|2Y7!prbSB_HourA1MhIHOO+RxCw}LejC+Uv8-o zaI6=?;|EShG0^T|!c#8rXqs(8<*WEMqa%HhdTLBZAAF)E3F zkEo@+Q~Ha7&MJrtjWYq2r^z^Exe7A}KS?VWo9QN2E;kd*Q_8P2`}s%)$`Y8U{G!mr zDX&2&sN{^mtKCqff;**WUJ#N|%IA$z8yG+_%4^-?ZKYF|ICbQB!K6PkzNL8e$P2Z; z1`X!X5{k;c){^_IpB7{xra?EvVN1^~1GIHllgPl&Fs}XAM)CU!u%tx%HJ7 zWOS{h!5mM zooL6UKWNciQ%l)iRlZ;v{ZgHmj*P#fzW>8GVSizN)FSf1tXNPpEq1#u0&U7N&XiwHKuBRyqYNgxoqA7UQjKhzV zsRtZXL@#{|g}gBNWa$FthadE?)LtvfhMl6WaT}G*_O?=72;i2jU^|nTJgnF~4RmHjVC3M+quP$kVVpqlwCysSAYuE#66}SZG$2&6zr@vKqU}WCJtr z2{YeQ=0FS^f5oXPa!4wANHB`mWweC$BXR%=DMS1iC)TlfWRlGfuN8NkECMpnjfK#R zE;5jkj#ByAu!c@KRg1vjd?HO!%$%B^P-c}RP^n{7S&7LHLHqx&nAa~6b0DWm$R97~ z%o#cRS^n~zs1HE{lPhB1ce$5(MRHz<#HVNl1ZJrQ^c_O0)x1QZm?eNDmd|gjK3iE` zX?@=O)NQ_ME^n-_JYRKN>rd7N?kg=c>BDy7cYaY!G-isq!A&A z#wz#$xqgN*(s!(%WOQ}Pr{Q_JtKzKQO%^~_XxsM`d2j$lZ4fD-1t3IFq+bF>=0WYq ze9PWyC;G9~gCiYq^s)y>2H;5l0UTKhj=recc*srmKqwp69Q*d5aNq7-w?Cvycw`ra z^4x_SdECuX#cLS8_q(PamXa2uB7&0pe7p zMrk3x!F|8LpZqy>1E9($&%J|a3h4}GUL?lhZ&XB{a)qkyX+ECCu+v=>m52A6eXk85 z`#gvNiigq5EYOQ$p@1+#nntM*n#6am4wtPUa(lSv+$1sSO1L$sTv+WFuU`>EvVzk5 zj46uS!T(fJ;6-J0tqjbwbReF>JSx7cVD?jD`~kX4$M^EG>+;yg*GDPES5&ku={_OJ zrA|dvESe*uE4}kOrymnb!x=FkP~atner^hUF-a=O`ABo$7hQr$1^w!dU)(e35~uD> z>awA3P5E@|$?B(1pTBZj&Gn5nmFCTsBuJJNW%x$xtv^{_UwPT=Zy-RCv($Y=yG$PV zX>M-Dai}m^-9T~!-(xm1g(-lOJ%tvyA46cX)snkEFOf0Nbm8)IWe a-NbLCUM{h!-fKp^LiA)^W7B17^~(3kv>q$~ diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py deleted file mode 100644 index 531da57..0000000 --- a/cloudshell/sandbox_rest/helpers/polling_helpers.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Helper functions to poll the state of sandbox setup or the execution status of a command. -Using 'retrying' library to do polling: -https://pypi.org/project/retrying/ -""" -from enum import Enum -from typing import Callable, List - -from retrying import RetryError, retry # pip install retrying - -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession - - -class OrchestrationPollingTimeout(Exception): - pass - - -class CommandPollingTimeout(Exception): - pass - - -class SandboxStates(Enum): - BEFORE_SETUP_STATE = "BeforeSetup" - RUNNING_SETUP_STATE = "Setup" - error_state = "Error" - ready_state = "Ready" - teardown_state = "Teardown" - ended_state = "Ended" - - -class ExecutionStatuses(Enum): - running_status = "Running" - pending_status = "Pending" - completed_status = "Completed" - failed_status = "Failed" - - -SANDBOX_SETUP_STATES = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value] - -SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value, SandboxStates.error_state.value] - -UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value, ExecutionStatuses.pending_status.value] - - -def _should_we_keep_polling_setup(sandbox_details: dict) -> bool: - """ if still in setup keep polling """ - setup_states = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value] - if sandbox_details["state"] in setup_states: - return True - return False - - -def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool: - """ if in teardown keep polling """ - if sandbox_details["state"] == SandboxStates.teardown_state.value: - return True - return False - - -def _poll_sandbox_state( - api: SandboxRestApiSession, - reservation_id: str, - polling_func: Callable, - max_polling_minutes: int, - polling_frequency_seconds: int, -) -> str: - """ Create blocking polling process """ - - # retry wait times are in milliseconds - @retry( - retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000, stop_max_delay=max_polling_minutes * 60000 - ) - def get_sandbox_details(): - return api.get_sandbox_details(reservation_id) - - try: - sandbox_details = get_sandbox_details() - except RetryError: - raise OrchestrationPollingTimeout(f"Sandbox Polling timed out after configured {max_polling_minutes} minutes") - return sandbox_details - - -def poll_sandbox_setup( - api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30 -) -> dict: - """ poll until completion """ - sandbox_details = _poll_sandbox_state( - api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, polling_frequency_seconds - ) - return sandbox_details - - -def poll_sandbox_teardown( - api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30 -) -> dict: - """ poll until completion """ - sandbox_details = _poll_sandbox_state( - api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes, polling_frequency_seconds - ) - return sandbox_details - - -def _should_we_keep_polling_execution(exc_data: dict) -> bool: - current_exc_status = exc_data["status"] - if current_exc_status in UNFINISHED_EXECUTION_STATUSES: - return True - return False - - -def poll_execution_for_completion( - sandbox_rest: SandboxRestApiSession, command_execution_id: str, max_polling_in_minutes=20, polling_frequency_in_seconds=30 -) -> str: - """ - poll execution for "Completed" status, then return the execution output - """ - - # retry wait times are in milliseconds - @retry( - retry_on_result=_should_we_keep_polling_execution, - wait_fixed=polling_frequency_in_seconds * 1000, - stop_max_delay=max_polling_in_minutes * 60000, - ) - def get_execution_data(): - exc_data = sandbox_rest.get_execution_details(command_execution_id) - return exc_data - - try: - exc_data = get_execution_data(sandbox_rest, command_execution_id) - except RetryError: - raise CommandPollingTimeout(f"Execution polling timed out after max {max_polling_in_minutes} minutes") - return exc_data diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 97b090f..0a5cd18 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -1,26 +1,8 @@ import json from dataclasses import asdict, dataclass from typing import List - import requests - - -class SandboxRestException(Exception): - """ General exception to raise inside Rest client class """ - - pass - - -class SandboxRestAuthException(Exception): - """ Failed auth actions """ - - pass - - -class SandboxRestInitException(ValueError): - """ Failed auth actions """ - - pass +from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException @dataclass @@ -40,9 +22,8 @@ class SandboxRestApiSession: View http:///api/v2/explore to see schemas of return json values """ - def __init__( - self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2" - ): + def __init__(self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, + api_version="v2"): """ login to api and store headers for future requests """ _protocol = "https" if is_https else "http" self._base_url = f"{_protocol}://{host}:{port}/api" @@ -73,15 +54,15 @@ def _build_auth_headers(login_token: str) -> dict: auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"} return auth_headers - def refresh_auth_from_stored_credentials(self): + def refresh_auth_from_stored_credentials(self) -> None: self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) self._auth_headers = self._build_auth_headers(self.auth_token) - def invalidate_auth(self): + def invalidate_auth(self) -> None: self.delete_token(self.auth_token) self._auth_headers = None - def _validate_auth_headers(self): + def _validate_auth_headers(self) -> None: if not self._auth_headers: raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session") @@ -90,16 +71,16 @@ def get_token_with_credentials(self, user_name: str, password: str, domain: str) """ Get token from credentials - extraneous quotes stripped off token string """ - login_res = requests.put( + response = requests.put( url=f"{self._base_url}/login", data=json.dumps({"username": user_name, "password": password, "domain": domain}), headers={"Content-Type": "application/json"}, ) - if not login_res.ok: - raise SandboxRestAuthException(self._format_err(login_res, "Failed Login")) + if not response.ok: + raise SandboxRestAuthException("Failed Login", response) - login_token = login_res.text[1:-1] + login_token = response.text[1:-1] return login_token def get_token_for_target_user(self, user_name: str) -> str: @@ -107,38 +88,30 @@ def get_token_for_target_user(self, user_name: str) -> str: Get token for target user - remove extraneous quotes """ self._validate_auth_headers() - login_res = requests.post( + response = requests.post( url=f"{self._base_url}/token", data=json.dumps({"username": user_name}), headers=self._auth_headers ) - if not login_res.ok: - raise SandboxRestException(self._format_err(login_res, f"Failed to get get token for user {user_name}")) + if not response.ok: + raise SandboxRestException(f"Failed to get get token for user {user_name}", response) - login_token = login_res.text[1:-1] + login_token = response.text[1:-1] return login_token def delete_token(self, token_id: str) -> None: self._validate_auth_headers() - login_res = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers) - if not login_res.ok: - raise SandboxRestException(self._format_err(login_res, "Failed to delete token")) - - def _format_err(self, response: requests.Response, custom_err_msg="Failed Api Call"): - response_data = ( - f"Response: {response.status_code}, Reason: {response.reason}\n" - f"Request URL: {response.request.url}\n" - f"Request Headers: {response.request.headers}" - ) - return f"Sandbox API Error for User: '{self.username}'.\n" f"{custom_err_msg}\n" f"{response_data}" + response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers) + if not response.ok: + raise SandboxRestException("Failed to delete token", response) # SANDBOX POST REQUESTS def start_sandbox( - self, - blueprint_id: str, - sandbox_name="", - duration="PT2H0M", - bp_params: List[InputParam] = None, - permitted_users: List[str] = None, + self, + blueprint_id: str, + sandbox_name="", + duration="PT2H0M", + bp_params: List[InputParam] = None, + permitted_users: List[str] = None, ) -> dict: """ Create a sandbox from the provided blueprint id @@ -162,13 +135,13 @@ def start_sandbox( custom_msg += "Ensure blueprint is 'Public' and in correct domain." if response.status_code >= 500: custom_msg += "Internal Server 500 Error During Request." - err_msg = self._format_err(response, custom_msg) - raise SandboxRestException(err_msg) + raise SandboxRestException(custom_msg, response) return response.json() def start_persistent_sandbox( - self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None - ): + self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, + permitted_users: List[str] = None + ) -> dict: """ Create a persistent sandbox from the provided blueprint id """ self._validate_auth_headers() url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" @@ -182,11 +155,12 @@ def start_persistent_sandbox( response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: - err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'") - raise SandboxRestException(err_msg) + err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'" + raise SandboxRestException(err_msg, response) return response.json() - def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True): + def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, + print_output=True) -> dict: """ Run a sandbox level command """ self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" @@ -195,12 +169,13 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I data_dict["params"] = params response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'")) + raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response) return response.json() def run_component_command( - self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True - ): + self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, + print_output: bool = True + ) -> dict: """ Start a command on sandbox component """ self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" @@ -209,10 +184,10 @@ def run_component_command( data_dict["params"] = params response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'")) + raise SandboxRestException(f"failed to start component command '{command_name}'", response) return response.json() - def extend_sandbox(self, sandbox_id: str, duration: str): + def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: """Extend the sandbox :param str sandbox_id: Sandbox id :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) @@ -221,42 +196,43 @@ def extend_sandbox(self, sandbox_id: str, duration: str): self._validate_auth_headers() data_dict = {"extended_time": duration} response = requests.post( - f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers ) if not response.ok: - raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'")) + raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response) return response.json() - def stop_sandbox(self, sandbox_id: str): + def stop_sandbox(self, sandbox_id: str) -> None: """ Stop the sandbox given sandbox id """ self._validate_auth_headers() response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'")) + raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response) # SANDBOX GET REQUESTS - def get_sandboxes(self, show_historic=False): + def get_sandboxes(self, show_historic=False) -> list: """ Get list of sandboxes """ self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes" params = {"show_historic": "true" if show_historic else "false"} response = requests.get(url, headers=self._auth_headers, params=params) if not response.ok: - err_msg = self._format_err(response, "Failed to get sandbox list") - raise SandboxRestException(err_msg) + err_msg = "Failed to get sandboxes" + raise SandboxRestException(err_msg, response) return response.json() - def get_sandbox_details(self, sandbox_id: str): + def get_sandbox_details(self, sandbox_id: str) -> dict: """ Get details of the given sandbox id """ self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}" response = requests.get(url, headers=self._auth_headers) if not response.ok: - exc_msg = self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'") - raise SandboxRestException(exc_msg) + exc_msg = f"Failed to get sandbox details for '{sandbox_id}'" + raise SandboxRestException(exc_msg, response) return response.json() - def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None): + def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, + tail: int = None) -> dict: """ Get list of sandbox activity 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') @@ -283,40 +259,38 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from response = requests.get(url, headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'")) + raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response) return response.json() - def get_sandbox_commands(self, sandbox_id: str): + def get_sandbox_commands(self, sandbox_id: str) -> list: """ Get list of sandbox commands """ self._validate_auth_headers() response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'")) + raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response) return response.json() - def get_sandbox_command_details(self, sandbox_id: str, command_name: str): + def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict: """ Get details of specific sandbox command """ self._validate_auth_headers() response = requests.get( f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers ) if not response.ok: - err_msg = self._format_err( - response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'" - ) - raise SandboxRestException(err_msg) + err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'" + raise SandboxRestException(err_msg, response) return response.json() - def get_sandbox_components(self, sandbox_id: str): + def get_sandbox_components(self, sandbox_id: str) -> list: """ Get list of sandbox components """ self._validate_auth_headers() response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'") - raise SandboxRestException(err_msg) + err_msg = (response, f"Failed to get components for '{sandbox_id}'") + raise SandboxRestException(err_msg, response) return response.json() - def get_sandbox_component_details(self, sandbox_id: str, component_id: str): + def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> dict: """ Get details of components in sandbox """ self._validate_auth_headers() response = requests.get( @@ -324,27 +298,29 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str): ) if not response.ok: custom_err_msg = ( - f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" + f"Failed to get sandbox component details.\n" + f"component id: '{component_id}'\n" + f"sandbox id: '{sandbox_id}'" ) - err_msg = self._format_err(response, custom_err_msg) - raise SandboxRestException(err_msg) + raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_component_commands(self, sandbox_id: str, component_id: str): + def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list: """ Get list of commands for a particular component in sandbox """ self._validate_auth_headers() response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", + headers=self._auth_headers ) if not response.ok: custom_err_msg = ( - f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'" - ) - err_msg = self._format_err(response, custom_err_msg) - raise SandboxRestException(err_msg) + f"Failed to get component commands.\n" + f"component id: '{component_id}'\n" + f"sandbox id: '{sandbox_id}'") + raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str): + def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict: """ Get details of a command of sandbox component """ self._validate_auth_headers() response = requests.get( @@ -352,26 +328,27 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s headers=self._auth_headers, ) if not response.ok: - custom_err_msg = ( - f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'" - ) - err_msg = self._format_err(response, custom_err_msg) - raise SandboxRestException(err_msg) + custom_err_msg = (f"Failed to get command details.\n " + f"component id: '{component_id}'\n" + f"sandbox id: '{sandbox_id}'") + raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_instructions(self, sandbox_id: str): + def get_sandbox_instructions(self, sandbox_id: str) -> str: """ pull the instruction text of sandbox """ self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", + headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") - raise SandboxRestException(err_msg) + err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'" + raise SandboxRestException(err_msg, response) return response.json() - def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None): + def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, + since: str = None) -> dict: """ Get list of sandbox output """ self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions" + url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output" params = {} if tail: params["tail"] = tail @@ -386,20 +363,20 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i response = requests.get(url, headers=self._auth_headers) if not response.ok: - err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'") - raise SandboxRestException(err_msg) + err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'" + raise SandboxRestException(err_msg, response) return response.json() # BLUEPRINT GET REQUESTS - def get_blueprints(self): + def get_blueprints(self) -> list: """ Get list of blueprints """ self._validate_auth_headers() response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, "Failed to get blueprints")) + raise SandboxRestException("Failed to get blueprints", response) return response.json() - def get_blueprint_details(self, blueprint_id: str): + def get_blueprint_details(self, blueprint_id: str) -> dict: """ Get details of a specific blueprint Can pass either blueprint name OR blueprint ID @@ -407,39 +384,20 @@ def get_blueprint_details(self, blueprint_id: str): self._validate_auth_headers() response = requests.get(f"{self._versioned_url}/blueprints/{blueprint_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'")) + raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response) return response.json() # EXECUTIONS - def get_execution_details(self, execution_id: str): + def get_execution_details(self, execution_id: str) -> dict: self._validate_auth_headers() response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'")) + raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response) return response.json() - def delete_execution(self, execution_id: str): + def delete_execution(self, execution_id: str) -> dict: self._validate_auth_headers() response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: - raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'")) + raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) return response.json() - - -if __name__ == "__main__": - TARGET_END_USER = "natti" - TARGET_DOMAIN = "end_users" - TARGET_DOMAIN = "Global" - - # admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN) - admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN) - from cloudshell.api.cloudshell_api import CloudShellAPISession - - with admin_api: - all_sandboxes = admin_api.get_sandboxes() - print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}") - - end_user_token = admin_api.get_token_for_target_user(TARGET_END_USER) - with SandboxRestApiSession(host="localhost", username=TARGET_END_USER, token=end_user_token) as user_rest: - user_bps = user_rest.get_blueprints() - print(f"Blueprint count pulled by user '{TARGET_END_USER}': {len(user_bps)}") diff --git a/cloudshell/sandbox_rest/sandbox_components.py b/cloudshell/sandbox_rest/sandbox_components.py deleted file mode 100644 index 7325eab..0000000 --- a/cloudshell/sandbox_rest/sandbox_components.py +++ /dev/null @@ -1,102 +0,0 @@ -import json -from enum import Enum -from typing import List - - -class ComponentTypes(Enum): - app_type = "Application" - resource_type = "Resource" - service_type = "Service" - - -class AppLifeCycleTypes(Enum): - deployed = "Deployed" - un_deployed = "Undeployed" - - -class AttributeTypes(Enum): - boolean_type = "boolean" - password_type = "password" - string_type = "string" - numeric_type = "numeric" - - -class SandboxRestComponents: - def __init__(self, components: List[dict] = None): - """ - if instantiated with components then populate lists - if not, then call "refresh_components" after getting data from sandbox - """ - self.all_components = components if components else [] - self.services = [] - self.resources = [] - self.deployed_apps = [] - self.un_deployed_apps = [] - if self.all_components: - self._sort_components() - - def _filter_components_by_type(self, component_type: str) -> List[dict]: - """ accepts both short info components and full info """ - return [component for component in self.all_components if component["type"] == component_type] - - def _filter_app_by_lifecycle(self, lifecycle_type): - return [ - component - for component in self.all_components - if component["type"] == ComponentTypes.app_type.value and component["app_lifecyle"] == lifecycle_type - ] - - def _sort_components(self) -> None: - """ sort stored components into separate lists """ - self.resources = self._filter_components_by_type(ComponentTypes.resource_type.value) - self.services = self._filter_components_by_type(ComponentTypes.service_type.value) - self.deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.deployed.value) - self.un_deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.un_deployed.value) - - def refresh_components(self, components: List[dict]) -> None: - self.all_components = components - self._sort_components() - - def find_component_by_name(self, component_name: str) -> dict: - for component in self.all_components: - if component["name"] == component_name: - return component - - @staticmethod - def filter_by_model(components: List[dict], model: str) -> List[dict]: - """ - Can pass in all components or sub list - Both Resources and Applications can use same shell / model - """ - return [component for component in components if component["component_type"] == model] - - def filter_by_attr_value(self, components: List[dict], attribute_name: str, attribute_value: str) -> List[dict]: - """ attribute name does not have to include model / family namespace """ - self._validate_components_for_attributes(components) - result = [] - for component in components: - for attr in component["attributes"]: - if attr["name"].endswith(attribute_name) and attr["value"] == attribute_value: - result.append(component) - return result - - def filter_by_boolean_attr_true(self, components: List[dict], attribute_name: str) -> List[dict]: - """ attribute name does not have to include model / family namespace """ - self._validate_components_for_attributes(components) - result = [] - for component in components: - for attr in component["attributes"]: - if attr["name"].endswith(attribute_name) and attr["value"].lower() == "true": - result.append(component) - return result - - @staticmethod - def _validate_components_for_attributes(components: List[dict]): - if not components: - return - attrs = components[0].get("attributes") - if not attrs: - raise Exception( - "'attributes' member not found. Must pass in Full info components.\n" - f"components data passed:\n{json.dumps(components, indent=4)}" - ) diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py deleted file mode 100644 index bc79418..0000000 --- a/cloudshell/sandbox_rest/sandbox_controller.py +++ /dev/null @@ -1,185 +0,0 @@ -import json -from timeit import default_timer -from typing import List - -from cloudshell.logger.qs_logger import get_qs_logger -from cloudshell.sandbox_rest.helpers.polling_helpers import ( - SANDBOX_ACTIVE_STATES, - SANDBOX_SETUP_STATES, - SandboxStates, - poll_execution_for_completion, - poll_sandbox_setup, - poll_sandbox_teardown, -) -from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession -from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents - - -class SandboxSetupError(Exception): - """ When sandbox has error during setup """ - - pass - - -class SandboxTeardownError(Exception): - """ When sandbox has error during setup """ - - pass - - -class CommandFailedException(Exception): - pass - - -class SandboxRestController: - def __init__(self, api: SandboxRestApiSession, sandbox_id="", disable_prints=False): - self.api = api - self.sandbox_id = sandbox_id - self.components = SandboxRestComponents() - self.setup_finished = False - self.sandbox_ended = False - self.setup_errors: List[dict] = None - self.teardown_errors: List[dict] = None - self.disable_prints = disable_prints - - # when passing in existing sandbox id update instance state, otherwise let "start sandbox" handle it - if self.sandbox_id: - self._handle_sandbox_id_on_init() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if exc_type: - err_msg = f"Exiting sandbox scope with error. f{exc_type}: {exc_val}" - self._print(err_msg) - - def _handle_sandbox_id_on_init(self): - self.update_components() - self.update_state_flags() - - def update_components(self): - components = self.api.get_sandbox_components(self.sandbox_id) - self.components.refresh_components(components) - - def update_state_flags(self): - details = self.api.get_sandbox_details(self.sandbox_id) - sandbox_state = details["state"] - if sandbox_state not in SANDBOX_SETUP_STATES: - self.setup_finished = True - elif sandbox_state not in SANDBOX_ACTIVE_STATES: - self.sandbox_ended = True - - def _print(self, message): - if not self.disable_prints: - print(message) - - def start_sandbox_and_poll( - self, - blueprint_id: str, - sandbox_name="", - duration="PT1H30M", - bp_params: List[InputParam] = None, - permitted_users: List[str] = None, - max_polling_minutes=30, - raise_setup_exception=True, - ) -> dict: - """ Start sandbox, poll for result, get back sandbox info """ - if self.sandbox_id: - raise ValueError( - f"Sandbox already has id '{self.sandbox_id}'.\n" f"Start blueprint with new sandbox controller instance" - ) - - self._print(f"Starting blueprint {blueprint_id}") - start = default_timer() - start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users) - self.sandbox_id = start_response["id"] - sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes, polling_frequency_seconds=30) - self.update_components() - sandbox_state = sandbox_details["state"] - stage = sandbox_details["setup_stage"] - name = sandbox_details["name"] - - if sandbox_state == SandboxStates.ready_state.value: - self.setup_finished = True - - # scan activity feed for errors - if sandbox_state == SandboxStates.error_state.value: - self.setup_finished = True - activity_errors = self._get_all_activity_errors(self.sandbox_id) - if activity_errors: - # print and store setup error data - self.setup_errors = activity_errors - err_msg = f"Error Events during setup:\n{json.dumps(activity_errors, indent=4)}" - self._print(err_msg) - - if raise_setup_exception: - err_msg = ( - f"Sandbox '{name}' Error during SETUP. See events for details.\n" - f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'" - ) - raise SandboxSetupError(err_msg) - - total_minutes = (default_timer() - start) / 60 - self._print(f"Setup finished after {total_minutes:.2f} minutes.") - return sandbox_details - - def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_teardown_exception=True) -> dict: - if self.sandbox_ended: - raise ValueError(f"sandbox {self.sandbox_id} already in completed state") - - last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"] - start = default_timer() - self.api.stop_sandbox(sandbox_id) - sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, polling_frequency_seconds=30) - self.update_components() - sandbox_state = sandbox_details["state"] - stage = sandbox_details["setup_stage"] - name = sandbox_details["name"] - - if sandbox_state == SandboxStates.ended_state.value: - self.sandbox_ended = True - - tail_error_count = 5 - tailed_errors = self.api.get_sandbox_activity( - sandbox_id, error_only=True, from_event_id=last_activity_id + 1, tail=tail_error_count - )["events"] - if tailed_errors: - self.sandbox_ended = True - self.teardown_errors = tailed_errors - self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}") - - if raise_teardown_exception: - err_msg = ( - f"Sandbox '{name}' Error during SETUP.\n" - f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'" - ) - raise SandboxTeardownError(err_msg) - - total_minutes = (default_timer() - start) / 60 - self._print(f"Teardown done in {total_minutes:.2f} minutes.") - return sandbox_details - - def rerun_setup(self): - self - pass - - def run_sandbox_command_and_poll(self): - pass - - def run_component_command_and_poll(self): - pass - - def _get_all_activity_errors(self, sandbox_id): - return self.api.get_sandbox_activity(sandbox_id, error_only=True)["events"] - - def publish_sandbox_id_to_env_vars(self): - """ publish sandbox id as environment variable for different CI process to pick up """ - pass - - -if __name__ == "__main__": - api = SandboxRestApiSession("localhost", "admin", "admin") - controller = SandboxRestController(api) - response = controller.start_sandbox_and_poll("rest test", "lolol") - print(json.dumps(response, indent=4)) diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO index df7ebb0..3e4c6be 100644 --- a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO +++ b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO @@ -1,4 +1,4 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: cloudshell-sandboxapi-wrapper Version: 2.0.0 Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI @@ -6,36 +6,38 @@ Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python Author: sadanand.s Author-email: sadanand.s@quali.com License: Apache 2.0 -Description: CloudShell Sandbox API Wrapper - ============================== - - Installation - ************* - :: - - pip install cloudshell_sandboxapi_wrapper - - Example Usage - ************** - :: - - from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI - sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT) - blueprints = sandbox.get_blueprints() - blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id'] - sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME) - sandbox.stop_sandbox(sandbox_id) - - | - - :Note: - Tested on cloudshell 9.3 with Python 2.7/3.7/3.8. - For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API `_ - - | Keywords: cloudshell,sandbox,api,CI Platform: UNKNOWN Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 + +CloudShell Sandbox API Wrapper +============================== + +Installation +************* +:: + + pip install cloudshell_sandboxapi_wrapper + +Example Usage +************** +:: + + from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI + sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT) + blueprints = sandbox.get_blueprints() + blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id'] + sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME) + sandbox.stop_sandbox(sandbox_id) + +| + +:Note: + Tested on cloudshell 9.3 with Python 2.7/3.7/3.8. + For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API `_ + +| + diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt index 1998237..94eb6d5 100644 --- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt +++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt @@ -3,10 +3,8 @@ setup.py cloudshell/__init__.py cloudshell/sandbox_rest/__init__.py cloudshell/sandbox_rest/sandbox_api.py -cloudshell/sandbox_rest/sandbox_components.py -cloudshell/sandbox_rest/sandbox_controller.py -cloudshell/sandbox_rest/helpers/__init__.py -cloudshell/sandbox_rest/helpers/polling_helpers.py +cloudshell/sandbox_rest/exceptions/__init__.py +cloudshell/sandbox_rest/exceptions/sandbox_api_exceptions.py cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt diff --git a/test-requirements.txt b/test-requirements.txt index 55b033e..8c99e88 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ -pytest \ No newline at end of file +pytest +python-dotenv \ No newline at end of file diff --git a/tests/common.py b/tests/common.py index 8539c15..499e09d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,9 +1,5 @@ import json -DUT_RESOURCE = "DUT_1" -DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" -HEALTH_CHECK_COMMAND = "health_check" - def pretty_print_response(dict_response): json_str = json.dumps(dict_response, indent=4) diff --git a/tests/common_fixtures.py b/tests/common_fixtures.py deleted file mode 100644 index 5dcb85f..0000000 --- a/tests/common_fixtures.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession - - -@pytest.fixture -def dut_bp_name() -> str: - """ For testing basic operations """ - return "DUT Test" - - -@pytest.fixture -def dut_resource_name() -> str: - """ For testing basic operations """ - return "DUT_1" - - -@pytest.fixture -def admin_session() -> SandboxRestApiSession: - return SandboxRestApiSession("localhost", "admin", "admin") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..99b4aaf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import time + +import pytest +from env_settings import * +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession + + +@pytest.fixture(scope="session") +def admin_session() -> SandboxRestApiSession: + with SandboxRestApiSession(CLOUDSHELL_SERVER, CLOUDSHELL_ADMIN_USER, CLOUDSHELL_ADMIN_PASSWORD) as api: + yield api + time.sleep(2) + print("admin session token revoked") + diff --git a/tests/env_settings.py b/tests/env_settings.py new file mode 100644 index 0000000..8d05309 --- /dev/null +++ b/tests/env_settings.py @@ -0,0 +1,22 @@ +""" +create .env file in tests directory with following keys: +CLOUDSHELL_ADMIN_USER=admin +CLOUDSHELL_ADMIN_PASSWORD=admin +CLOUDSHELL_SERVER=localhost +""" + +import os +from dotenv import load_dotenv + +load_dotenv() + +# server credentials from .env +CLOUDSHELL_ADMIN_USER = os.environ.get("CLOUDSHELL_ADMIN_USER") +CLOUDSHELL_ADMIN_PASSWORD = os.environ.get("CLOUDSHELL_ADMIN_PASSWORD") +CLOUDSHELL_SERVER = os.environ.get("CLOUDSHELL_SERVER") + + +DUT_RESOURCE = "DUT_1" +DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" +HEALTH_CHECK_COMMAND = "health_check" + diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py index 6ea9528..87856bc 100644 --- a/tests/test_api _no_sandbox.py +++ b/tests/test_api _no_sandbox.py @@ -1,14 +1,12 @@ """ -Test the api methods that do not require a live sandbox +Functionally test the api methods that do not require a live sandbox - get all blueprints - get all sandboxes - get blueprint by id - get token + delete token """ -from common import DEFAULT_BLUEPRINT_TEMPLATE, pretty_print_response -from common_fixtures import admin_session - from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +from env_settings import DEFAULT_BLUEPRINT_TEMPLATE def test_get_sandboxes(admin_session: SandboxRestApiSession): diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py new file mode 100644 index 0000000..bad3cc7 --- /dev/null +++ b/tests/test_api_empty_sandbox.py @@ -0,0 +1,67 @@ +""" +Test the api methods that require a live sandbox against default +- start sandbox +""" +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +from env_settings import DEFAULT_BLUEPRINT_TEMPLATE +import pytest + + +@pytest.fixture(scope="module") +def sandbox_id(admin_session: SandboxRestApiSession): + # start sandbox + start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, + sandbox_name="Pytest empty blueprint test") + sandbox_id = start_res["id"] + print(f"Sandbox started: {sandbox_id}") + yield sandbox_id + admin_session.stop_sandbox(sandbox_id) + print(f"\nSandbox ended: {sandbox_id}") + + +def test_start_stop(admin_session, sandbox_id): + pass + + +def test_get_sandbox_details(admin_session, sandbox_id): + details_res = admin_session.get_sandbox_details(sandbox_id) + sb_name = details_res["name"] + print(f"Pulled details for sandbox '{sb_name}'") + + +def test_get_components(admin_session, sandbox_id): + sb_components = admin_session.get_sandbox_components(sandbox_id) + component_count = len(sb_components) + print(f"component count found: {component_count}") + + +def test_get_sandbox_commands(admin_session, sandbox_id): + sb_commands = admin_session.get_sandbox_commands(sandbox_id) + print(f"Sandbox commands: {[x['name'] for x in sb_commands]}") + first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, sb_commands[0]["name"]) + print(f"SB command name: {first_sb_command['name']}\n" + f"description: {first_sb_command['description']}") + + +def test_get_sandbox_events(admin_session, sandbox_id): + activity = admin_session.get_sandbox_activity(sandbox_id) + events = activity["events"] + print(f"activity events count: {len(events)}") + + +def test_get_console_output(admin_session, sandbox_id): + sb_output = admin_session.get_sandbox_output(sandbox_id) + entries = sb_output["entries"] + print(f"Sandbox output entries count: {len(entries)}") + + +def test_get_instructions(admin_session, sandbox_id): + instructions = admin_session.get_sandbox_instructions(sandbox_id) + print(f"Pulled sandbox instructions: '{instructions}'") + + +def test_extend_sandbox(admin_session, sandbox_id): + extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M") + print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}") + + diff --git a/tests/test_api_with_sandbox.py b/tests/test_api_with_sandbox.py deleted file mode 100644 index 8c1f97a..0000000 --- a/tests/test_api_with_sandbox.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Test the api methods that require a live sandbox against default -- start sandbox -""" -from common import DEFAULT_BLUEPRINT_TEMPLATE -from common_fixtures import admin_session - -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession - - -def test_simple_flow(admin_session: SandboxRestApiSession): - """ """ - # start sandbox - start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest simple flow") - sandbox_id = start_res["id"] - assert sandbox_id - print(f"Sandbox started. ID: {sandbox_id}") - - # get sandbox details - details_res = admin_session.get_sandbox_details(sandbox_id) - sb_name = details_res["name"] - print(f"Pulled details for sandbox '{sb_name}'") - - # get components - should be empty - sb_components = admin_session.get_sandbox_components(sandbox_id) - component_count = len(sb_components) - print(f"component count found: {component_count}") - - # get activity events - - # get commands - - # diff --git a/tests/test_sandbox_controller.py b/tests/test_sandbox_controller.py deleted file mode 100644 index e69de29..0000000 From 038cd3f4483703410970306ee7d8b7a2c2df0012 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Tue, 26 Oct 2021 15:48:23 +0300 Subject: [PATCH 11/29] commiting tests --- .pre-commit-config.yaml | 6 +-- cloudshell/sandbox_rest/exceptions.py | 10 +++-- cloudshell/sandbox_rest/sandbox_api.py | 61 ++++++++++++-------------- test-requirements.txt | 4 +- tests/conftest.py | 2 +- tests/env_settings.py | 2 +- tests/test_api _no_sandbox.py | 3 +- tests/test_api_empty_sandbox.py | 15 +++---- 8 files changed, 53 insertions(+), 50 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3d0d87..d05e301 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,9 +36,9 @@ repos: language: system types: [python] args: [ - --max-line-length=127, - --max-public-methods=32, - --max-args=8, +# --max-line-length=127, +# --max-public-methods=32, +# --max-args=8, '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding', '--good-names=ip,rc,eval' ] \ No newline at end of file diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py index 634586d..f3bfa21 100644 --- a/cloudshell/sandbox_rest/exceptions.py +++ b/cloudshell/sandbox_rest/exceptions.py @@ -3,6 +3,7 @@ class SandboxRestException(Exception): """ Base Exception Class inside Rest client class """ + def __init__(self, msg: str, response: Response = None): self.msg = msg self.response = response @@ -18,11 +19,14 @@ def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = @staticmethod def _format_response_msg(response: Response): - return (f"Response: {response.status_code}, Reason: {response.reason}\n" - f"Request URL: {response.request.url}\n" - f"Request Headers: {response.request.headers}") + return ( + f"Response: {response.status_code}, Reason: {response.reason}\n" + f"Request URL: {response.request.url}\n" + f"Request Headers: {response.request.headers}" + ) class SandboxRestAuthException(SandboxRestException): """ Failed auth action """ + pass diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 0a5cd18..9bcf004 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -1,8 +1,10 @@ import json from dataclasses import asdict, dataclass from typing import List + import requests -from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException + +from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException @dataclass @@ -22,8 +24,9 @@ class SandboxRestApiSession: View http:///api/v2/explore to see schemas of return json values """ - def __init__(self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, - api_version="v2"): + def __init__( + self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2" + ): """ login to api and store headers for future requests """ _protocol = "https" if is_https else "http" self._base_url = f"{_protocol}://{host}:{port}/api" @@ -106,12 +109,12 @@ def delete_token(self, token_id: str) -> None: # SANDBOX POST REQUESTS def start_sandbox( - self, - blueprint_id: str, - sandbox_name="", - duration="PT2H0M", - bp_params: List[InputParam] = None, - permitted_users: List[str] = None, + self, + blueprint_id: str, + sandbox_name="", + duration="PT2H0M", + bp_params: List[InputParam] = None, + permitted_users: List[str] = None, ) -> dict: """ Create a sandbox from the provided blueprint id @@ -139,8 +142,7 @@ def start_sandbox( return response.json() def start_persistent_sandbox( - self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, - permitted_users: List[str] = None + self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None ) -> dict: """ Create a persistent sandbox from the provided blueprint id """ self._validate_auth_headers() @@ -159,8 +161,9 @@ def start_persistent_sandbox( raise SandboxRestException(err_msg, response) return response.json() - def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, - print_output=True) -> dict: + def run_sandbox_command( + self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True + ) -> dict: """ Run a sandbox level command """ self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" @@ -173,8 +176,7 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I return response.json() def run_component_command( - self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, - print_output: bool = True + self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True ) -> dict: """ Start a command on sandbox component """ self._validate_auth_headers() @@ -231,8 +233,9 @@ def get_sandbox_details(self, sandbox_id: str) -> dict: raise SandboxRestException(exc_msg, response) return response.json() - def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, - tail: int = None) -> dict: + def get_sandbox_activity( + self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None + ) -> dict: """ Get list of sandbox activity 'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') @@ -298,9 +301,7 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> d ) if not response.ok: custom_err_msg = ( - f"Failed to get sandbox component details.\n" - f"component id: '{component_id}'\n" - f"sandbox id: '{sandbox_id}'" + f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() @@ -309,14 +310,12 @@ def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> """ Get list of commands for a particular component in sandbox """ self._validate_auth_headers() response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", - headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers ) if not response.ok: custom_err_msg = ( - f"Failed to get component commands.\n" - f"component id: '{component_id}'\n" - f"sandbox id: '{sandbox_id}'") + f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" + ) raise SandboxRestException(custom_err_msg, response) return response.json() @@ -328,24 +327,22 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s headers=self._auth_headers, ) if not response.ok: - custom_err_msg = (f"Failed to get command details.\n " - f"component id: '{component_id}'\n" - f"sandbox id: '{sandbox_id}'") + custom_err_msg = ( + f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" + ) raise SandboxRestException(custom_err_msg, response) return response.json() def get_sandbox_instructions(self, sandbox_id: str) -> str: """ pull the instruction text of sandbox """ self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", - headers=self._auth_headers) + response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers) if not response.ok: err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'" raise SandboxRestException(err_msg, response) return response.json() - def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, - since: str = None) -> dict: + def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None) -> dict: """ Get list of sandbox output """ self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output" diff --git a/test-requirements.txt b/test-requirements.txt index 8c99e88..2ed62ba 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,4 @@ pytest -python-dotenv \ No newline at end of file +python-dotenv +flake8 +pylint \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 99b4aaf..81052bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import pytest from env_settings import * + from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @@ -11,4 +12,3 @@ def admin_session() -> SandboxRestApiSession: yield api time.sleep(2) print("admin session token revoked") - diff --git a/tests/env_settings.py b/tests/env_settings.py index 8d05309..51253ac 100644 --- a/tests/env_settings.py +++ b/tests/env_settings.py @@ -6,6 +6,7 @@ """ import os + from dotenv import load_dotenv load_dotenv() @@ -19,4 +20,3 @@ DUT_RESOURCE = "DUT_1" DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" HEALTH_CHECK_COMMAND = "health_check" - diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py index 87856bc..7200de1 100644 --- a/tests/test_api _no_sandbox.py +++ b/tests/test_api _no_sandbox.py @@ -5,9 +5,10 @@ - get blueprint by id - get token + delete token """ -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession from env_settings import DEFAULT_BLUEPRINT_TEMPLATE +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession + def test_get_sandboxes(admin_session: SandboxRestApiSession): res = admin_session.get_sandboxes() diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py index bad3cc7..8b24a77 100644 --- a/tests/test_api_empty_sandbox.py +++ b/tests/test_api_empty_sandbox.py @@ -2,16 +2,18 @@ Test the api methods that require a live sandbox against default - start sandbox """ -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession -from env_settings import DEFAULT_BLUEPRINT_TEMPLATE import pytest +from env_settings import DEFAULT_BLUEPRINT_TEMPLATE + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession): # start sandbox - start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, - sandbox_name="Pytest empty blueprint test") + start_res = admin_session.start_sandbox( + blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest empty blueprint test" + ) sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -39,8 +41,7 @@ def test_get_sandbox_commands(admin_session, sandbox_id): sb_commands = admin_session.get_sandbox_commands(sandbox_id) print(f"Sandbox commands: {[x['name'] for x in sb_commands]}") first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, sb_commands[0]["name"]) - print(f"SB command name: {first_sb_command['name']}\n" - f"description: {first_sb_command['description']}") + print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}") def test_get_sandbox_events(admin_session, sandbox_id): @@ -63,5 +64,3 @@ def test_get_instructions(admin_session, sandbox_id): def test_extend_sandbox(admin_session, sandbox_id): extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M") print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}") - - From b9597024fac5830a9541ebbd3fe197718244b697 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Thu, 28 Oct 2021 19:12:33 +0300 Subject: [PATCH 12/29] saving sample in main --- .gitignore | 3 ++- cloudshell/sandbox_rest/sandbox_api.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 25cf4a3..bdd9ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ .pytest_cache/ *.egg -tests/.env \ No newline at end of file +tests/.env +*.zip \ No newline at end of file diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 9bcf004..a50427a 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -25,7 +25,7 @@ class SandboxRestApiSession: """ def __init__( - self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2" + self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2" ): """ login to api and store headers for future requests """ _protocol = "https" if is_https else "http" @@ -398,3 +398,12 @@ def delete_execution(self, execution_id: str) -> dict: if not response.ok: raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) return response.json() + + +if __name__ == "__main__": + admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="end_users") + user_token = admin_api.get_token_for_target_user("end_user") + user_api = SandboxRestApiSession(host="localhost", token=user_token) + response = user_api.start_sandbox(blueprint_id="end user bp") + commands = user_api.get_sandbox_command_details() + pass \ No newline at end of file From e2f15dbcfe445eb49f6911aa9a22d33559f0c977 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Thu, 28 Oct 2021 19:14:09 +0300 Subject: [PATCH 13/29] black formatted --- cloudshell/sandbox_rest/sandbox_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index a50427a..2e0e2d6 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -406,4 +406,4 @@ def delete_execution(self, execution_id: str) -> dict: user_api = SandboxRestApiSession(host="localhost", token=user_token) response = user_api.start_sandbox(blueprint_id="end user bp") commands = user_api.get_sandbox_command_details() - pass \ No newline at end of file + pass From a6c162fa8a2ec231d64924776668a514dacc7fc6 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 00:14:51 +0200 Subject: [PATCH 14/29] updated docs --- README.MD | 20 +++++++++++++ README.rst | 27 ------------------ .../PKG-INFO | 4 +-- .../SOURCES.txt | 3 +- ...cloudshell_sandboxapi_wrapper-2.0.0.tar.gz | Bin 5103 -> 0 bytes setup.py | 5 ++-- 6 files changed, 26 insertions(+), 33 deletions(-) create mode 100644 README.MD delete mode 100644 README.rst delete mode 100644 dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..e64d887 --- /dev/null +++ b/README.MD @@ -0,0 +1,20 @@ +[![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python/) +[![PyPI version](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper.svg)](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper) + +# Cloudshell Sandbox API Wrapper +A python client implementation of the [cloudshell sandbox api](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/CS-Snbx-API-Topic.htm?Highlight=sandbox%20api). + +This client provides an object-oriented interface to instantiating an api session and interacting with the endpoints. +All methods return a loaded python dictionary/list of the [documented](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/RefGuides/Sndbx-REST-API/REST-API-V2-Ref-Guide.htm?tocpath=CloudShell%20API%20Guide%7CCloudShell%20Sandbox%20API%7C_____3) json responses. +No additional library object wrapping implemented. + + +### Installation + +``` +pip install cloudshell_sandboxapi_wrapper +``` +## Usage +```python +pass +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index 306cc41..0000000 --- a/README.rst +++ /dev/null @@ -1,27 +0,0 @@ -CloudShell Sandbox API Wrapper -============================== - -Installation -************* -:: - - pip install cloudshell_sandboxapi_wrapper - -Example Usage -************** -:: - - from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI - sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT) - blueprints = sandbox.get_blueprints() - blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id'] - sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME) - sandbox.stop_sandbox(sandbox_id) - -| - -:Note: - Tested on cloudshell 9.3 with Python 2.7/3.7/3.8. - For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API `_ - -| diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO index 3e4c6be..6968074 100644 --- a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO +++ b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO @@ -3,8 +3,8 @@ Name: cloudshell-sandboxapi-wrapper Version: 2.0.0 Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python -Author: sadanand.s -Author-email: sadanand.s@quali.com +Author: Quali +Author-email: support@qualisystems.com License: Apache 2.0 Keywords: cloudshell,sandbox,api,CI Platform: UNKNOWN diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt index 94eb6d5..f956ce2 100644 --- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt +++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt @@ -2,9 +2,8 @@ README.rst setup.py cloudshell/__init__.py cloudshell/sandbox_rest/__init__.py +cloudshell/sandbox_rest/exceptions.py cloudshell/sandbox_rest/sandbox_api.py -cloudshell/sandbox_rest/exceptions/__init__.py -cloudshell/sandbox_rest/exceptions/sandbox_api_exceptions.py cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt diff --git a/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz b/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz deleted file mode 100644 index 6424e232d889ab652ddd54d37f98245b742d9a01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5103 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 List[str]: setup( name="cloudshell_sandboxapi_wrapper", description="Python client for CloudShell Sandbox REST api - consume sandboxes via CI", - keywords=["cloudshell", "sandbox", "api", "CI"], + keywords=["cloudshell", "sandbox", "api", "rest", "CI"], url="https://github.com/QualiSystemsLab/Sandbox-API-Python", author="sadanand.s", author_email="sadanand.s@quali.com", license="Apache 2.0", packages=find_packages(), version=read_file("version.txt"), - long_description=read_file("README.rst"), + long_description=read_file("README.MD"), + long_description_content_type='text/markdown', install_requires=lines_from_file("requirements.txt"), test_requires=lines_from_file("test-requirements.txt"), classifiers=[ From e324a776f647f968dc7f341c1ae73511fce476b8 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 00:19:05 +0200 Subject: [PATCH 15/29] updated gitignore --- .gitignore | 4 +- .../PKG-INFO | 43 ------------------- .../SOURCES.txt | 11 ----- .../dependency_links.txt | 1 - .../requires.txt | 2 - .../top_level.txt | 1 - 6 files changed, 3 insertions(+), 59 deletions(-) delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/requires.txt delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/top_level.txt diff --git a/.gitignore b/.gitignore index bdd9ddf..9ae8ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea/ .pytest_cache/ *.egg +*.egg-info tests/.env -*.zip \ No newline at end of file +*.zip +dist/ \ No newline at end of file diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO deleted file mode 100644 index 6968074..0000000 --- a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO +++ /dev/null @@ -1,43 +0,0 @@ -Metadata-Version: 2.1 -Name: cloudshell-sandboxapi-wrapper -Version: 2.0.0 -Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI -Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python -Author: Quali -Author-email: support@qualisystems.com -License: Apache 2.0 -Keywords: cloudshell,sandbox,api,CI -Platform: UNKNOWN -Classifier: License :: OSI Approved :: Apache Software License -Classifier: Natural Language :: English -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 - -CloudShell Sandbox API Wrapper -============================== - -Installation -************* -:: - - pip install cloudshell_sandboxapi_wrapper - -Example Usage -************** -:: - - from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI - sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT) - blueprints = sandbox.get_blueprints() - blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id'] - sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME) - sandbox.stop_sandbox(sandbox_id) - -| - -:Note: - Tested on cloudshell 9.3 with Python 2.7/3.7/3.8. - For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API `_ - -| - diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt deleted file mode 100644 index f956ce2..0000000 --- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt +++ /dev/null @@ -1,11 +0,0 @@ -README.rst -setup.py -cloudshell/__init__.py -cloudshell/sandbox_rest/__init__.py -cloudshell/sandbox_rest/exceptions.py -cloudshell/sandbox_rest/sandbox_api.py -cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO -cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt -cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt -cloudshell_sandboxapi_wrapper.egg-info/requires.txt -cloudshell_sandboxapi_wrapper.egg-info/top_level.txt \ No newline at end of file diff --git a/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt b/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt deleted file mode 100644 index c3e8e97..0000000 --- a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests -retrying diff --git a/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt b/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt deleted file mode 100644 index 127ff4f..0000000 --- a/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -cloudshell From 29998c67284ed14024456f5a4a98b1f88c3e7ffe Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 14:35:26 +0200 Subject: [PATCH 16/29] all tests passing --- cloudshell/sandbox_rest/sandbox_api.py | 15 +++--- tests/common.py | 6 +++ tests/conftest.py | 18 ++++++- tests/constants.py | 7 +++ tests/env_settings.py | 7 +-- tests/test_api _no_sandbox.py | 17 +++--- tests/test_api_empty_sandbox.py | 75 +++++++++++++++++++------- tests/test_api_heath_check_sandbox.py | 70 ++++++++++++++++++++++++ tests/test_rerun_setup.py | 54 +++++++++++++++++++ 9 files changed, 230 insertions(+), 39 deletions(-) create mode 100644 tests/constants.py create mode 100644 tests/test_api_heath_check_sandbox.py create mode 100644 tests/test_rerun_setup.py diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 2e0e2d6..189c9dc 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -392,18 +392,21 @@ def get_execution_details(self, execution_id: str) -> dict: raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response) return response.json() - def delete_execution(self, execution_id: str) -> dict: + def delete_execution(self, execution_id: str) -> None: + """ + API returns dict with single key on successful deletion of execution + {"result": "success"} + """ self._validate_auth_headers() response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) if not response.ok: raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) - return response.json() + response_dict = response.json() + if not response_dict["result"] == "success": + raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" + f"Response: {response_dict}") if __name__ == "__main__": admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="end_users") user_token = admin_api.get_token_for_target_user("end_user") - user_api = SandboxRestApiSession(host="localhost", token=user_token) - response = user_api.start_sandbox(blueprint_id="end user bp") - commands = user_api.get_sandbox_command_details() - pass diff --git a/tests/common.py b/tests/common.py index 499e09d..0b97e70 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,6 +1,12 @@ import json +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession def pretty_print_response(dict_response): json_str = json.dumps(dict_response, indent=4) print(f"\n{json_str}") + + +def get_blueprint_id_from_name(api: SandboxRestApiSession, bp_name: str): + res = api.get_blueprint_details(bp_name) + return res["id"] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 81052bd..a95491e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,13 +2,29 @@ import pytest from env_settings import * +from constants import * from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @pytest.fixture(scope="session") def admin_session() -> SandboxRestApiSession: - with SandboxRestApiSession(CLOUDSHELL_SERVER, CLOUDSHELL_ADMIN_USER, CLOUDSHELL_ADMIN_PASSWORD) as api: + with SandboxRestApiSession(host=CLOUDSHELL_SERVER, + username=CLOUDSHELL_ADMIN_USER, + password=CLOUDSHELL_ADMIN_PASSWORD, + domain=CLOUDSHELL_DOMAIN) as api: yield api time.sleep(2) print("admin session token revoked") + + +@pytest.fixture(scope="session") +def empty_blueprint(): + return DEFAULT_EMPTY_BLUEPRINT + + +@pytest.fixture(scope="session") +def dut_blueprint(): + return DUT_BLUEPRINT + + diff --git a/tests/constants.py b/tests/constants.py new file mode 100644 index 0000000..77becad --- /dev/null +++ b/tests/constants.py @@ -0,0 +1,7 @@ +DEFAULT_EMPTY_BLUEPRINT = "CloudShell Sandbox Template" +BLUEPRINT_SETUP_COMMAND = "Setup" + +# DUT blueprint constants +DUT_MODEL = "Putshell" +DUT_COMMAND = "health_check" +DUT_BLUEPRINT = "DUT Blueprint Test" diff --git a/tests/env_settings.py b/tests/env_settings.py index 51253ac..306daa0 100644 --- a/tests/env_settings.py +++ b/tests/env_settings.py @@ -3,6 +3,7 @@ CLOUDSHELL_ADMIN_USER=admin CLOUDSHELL_ADMIN_PASSWORD=admin CLOUDSHELL_SERVER=localhost +CLOUDSHELL_DOMAIN=Global """ import os @@ -15,8 +16,4 @@ CLOUDSHELL_ADMIN_USER = os.environ.get("CLOUDSHELL_ADMIN_USER") CLOUDSHELL_ADMIN_PASSWORD = os.environ.get("CLOUDSHELL_ADMIN_PASSWORD") CLOUDSHELL_SERVER = os.environ.get("CLOUDSHELL_SERVER") - - -DUT_RESOURCE = "DUT_1" -DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template" -HEALTH_CHECK_COMMAND = "health_check" +CLOUDSHELL_DOMAIN = os.environ.get("CLOUDSHELL_DOMAIN") diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py index 7200de1..f95d6b5 100644 --- a/tests/test_api _no_sandbox.py +++ b/tests/test_api _no_sandbox.py @@ -1,27 +1,27 @@ """ -Functionally test the api methods that do not require a live sandbox -- get all blueprints -- get all sandboxes -- get blueprint by id -- get token + delete token +Test the api methods that do NOT require a blueprint +Live Cloudshell server is still a dependency """ -from env_settings import DEFAULT_BLUEPRINT_TEMPLATE +from constants import DEFAULT_EMPTY_BLUEPRINT from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession def test_get_sandboxes(admin_session: SandboxRestApiSession): res = admin_session.get_sandboxes() + assert(type(res) is list) print(f"Sandbox count found in system: {len(res)}") def test_get_blueprints(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprints() + assert(type(bp_res) is list) print(f"Blueprint count found in system: '{len(bp_res)}'") def test_get_default_blueprint(admin_session: SandboxRestApiSession): - bp_res = admin_session.get_blueprint_details(DEFAULT_BLUEPRINT_TEMPLATE) + bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT) + assert(type(bp_res) is dict) bp_name = bp_res["name"] print(f"Pulled details for '{bp_name}'") @@ -29,6 +29,5 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession): def test_get_and_delete_token(admin_session: SandboxRestApiSession): """ get token for admin user """ token_res = admin_session.get_token_for_target_user("admin") + assert(type(token_res) is str) print(f"Token response: '{token_res}'") - admin_session.delete_token(token_res) - print("deleted token") diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py index 8b24a77..93edde5 100644 --- a/tests/test_api_empty_sandbox.py +++ b/tests/test_api_empty_sandbox.py @@ -1,18 +1,26 @@ """ -Test the api methods that require a live sandbox against default -- start sandbox +Test the api methods that require an empty, PUBLIC blueprint """ -import pytest -from env_settings import DEFAULT_BLUEPRINT_TEMPLATE +import time +import pytest from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +from constants import * +from common import * + + +@pytest.fixture(scope="module") +def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint): + res_id = get_blueprint_id_from_name(admin_session, empty_blueprint) + assert(type(res_id) is str) + return res_id @pytest.fixture(scope="module") -def sandbox_id(admin_session: SandboxRestApiSession): +def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox start_res = admin_session.start_sandbox( - blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest empty blueprint test" + blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test" ) sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") @@ -21,46 +29,77 @@ def sandbox_id(admin_session: SandboxRestApiSession): print(f"\nSandbox ended: {sandbox_id}") +@pytest.fixture(scope="module") +def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): + polling_minutes = 2 + counter = 0 + while True: + if counter > polling_minutes: + raise Exception("Timeout waiting for setup to end") + state = admin_session.get_sandbox_details(sandbox_id)["state"] + if state == "Ready": + break + time.sleep(60) + counter += 1 + + print("Rerunning Setup...") + res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, + command_name=BLUEPRINT_SETUP_COMMAND) + assert (type(res) is dict) + print("Setup re-run execution response") + pretty_print_response(res) + execution_id = res["executionId"] + return execution_id + + def test_start_stop(admin_session, sandbox_id): - pass + assert (type(sandbox_id) is str) + print(f"Sandbox ID: {sandbox_id}") def test_get_sandbox_details(admin_session, sandbox_id): details_res = admin_session.get_sandbox_details(sandbox_id) + assert (type(details_res) is dict) sb_name = details_res["name"] print(f"Pulled details for sandbox '{sb_name}'") def test_get_components(admin_session, sandbox_id): - sb_components = admin_session.get_sandbox_components(sandbox_id) - component_count = len(sb_components) + components_res = admin_session.get_sandbox_components(sandbox_id) + assert (type(components_res) is list) + component_count = len(components_res) print(f"component count found: {component_count}") def test_get_sandbox_commands(admin_session, sandbox_id): - sb_commands = admin_session.get_sandbox_commands(sandbox_id) - print(f"Sandbox commands: {[x['name'] for x in sb_commands]}") - first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, sb_commands[0]["name"]) + commands_res = admin_session.get_sandbox_commands(sandbox_id) + assert (type(commands_res) is list) + print(f"Sandbox commands: {[x['name'] for x in commands_res]}") + first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"]) print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}") def test_get_sandbox_events(admin_session, sandbox_id): - activity = admin_session.get_sandbox_activity(sandbox_id) - events = activity["events"] + activity_res = admin_session.get_sandbox_activity(sandbox_id) + assert (type(activity_res) is dict and "events" in activity_res) + events = activity_res["events"] print(f"activity events count: {len(events)}") def test_get_console_output(admin_session, sandbox_id): - sb_output = admin_session.get_sandbox_output(sandbox_id) - entries = sb_output["entries"] + output_res = admin_session.get_sandbox_output(sandbox_id) + assert (type(output_res) is dict and "entries" in output_res) + entries = output_res["entries"] print(f"Sandbox output entries count: {len(entries)}") def test_get_instructions(admin_session, sandbox_id): - instructions = admin_session.get_sandbox_instructions(sandbox_id) - print(f"Pulled sandbox instructions: '{instructions}'") + instructions_res = admin_session.get_sandbox_instructions(sandbox_id) + assert (type(instructions_res) is str) + print(f"Pulled sandbox instructions: '{instructions_res}'") def test_extend_sandbox(admin_session, sandbox_id): extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M") + assert (type(extend_response) is dict and "remaining_time" in extend_response) print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}") diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_heath_check_sandbox.py new file mode 100644 index 0000000..25a47fc --- /dev/null +++ b/tests/test_api_heath_check_sandbox.py @@ -0,0 +1,70 @@ +""" +Test the api methods against blueprint with a resource containing a command +- Putshell mock can be used - https://community.quali.com/repos/3318/put-shell-mock +- DUT model / command can be referenced in constants.py (Putshell / health_check) +- Assumed that only one DUT is in blueprint +""" +import time + +import pytest +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +from constants import * +from common import * + + +@pytest.fixture(scope="module") +def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint): + res_id = get_blueprint_id_from_name(admin_session, dut_blueprint) + assert(type(res_id) is str) + return res_id + + +@pytest.fixture(scope="module") +def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): + # start sandbox + start_res = admin_session.start_sandbox( + blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test" + ) + sandbox_id = start_res["id"] + print(f"Sandbox started: {sandbox_id}") + yield sandbox_id + admin_session.stop_sandbox(sandbox_id) + print(f"\nSandbox ended: {sandbox_id}") + + +@pytest.fixture(scope="module") +def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str): + components = admin_session.get_sandbox_components(sandbox_id) + component_filter = [x for x in components if x["component_type"] == DUT_MODEL] + assert component_filter + return component_filter[0]["id"] + + +@pytest.fixture(scope="module") +def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str): + print("Starting DUT Command...") + res = admin_session.run_component_command(sandbox_id=sandbox_id, + component_id=component_id, + command_name=DUT_COMMAND) + assert(type(res) is dict) + print("Started execution response") + pretty_print_response(res) + execution_id = res["executionId"] + return execution_id + + +@pytest.fixture(scope="module") +def test_get_execution_details(admin_session, execution_id): + res = admin_session.get_execution_details(execution_id) + assert(type(res) is dict) + print("Execution Details") + pretty_print_response(res) + + +def test_delete_execution(admin_session, execution_id, test_get_execution_details): + print("Stopping execution...") + time.sleep(1) + admin_session.delete_execution(execution_id) + print("Execution deleted") + + diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py new file mode 100644 index 0000000..8827729 --- /dev/null +++ b/tests/test_rerun_setup.py @@ -0,0 +1,54 @@ +""" +Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution, finally end sandbox +""" +import time + +import pytest +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +from constants import * +from common import * + + +@pytest.fixture(scope="module") +def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint): + # start sandbox + start_res = admin_session.start_sandbox( + blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test" + ) + sandbox_id = start_res["id"] + print(f"Sandbox started: {sandbox_id}") + yield sandbox_id + admin_session.stop_sandbox(sandbox_id) + print(f"\nSandbox ended: {sandbox_id}") + + +@pytest.fixture(scope="module") +def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): + polling_minutes = 2 + counter = 0 + while True: + if counter > polling_minutes: + raise Exception("Timeout waiting for setup to end") + state = admin_session.get_sandbox_details(sandbox_id)["state"] + if state == "Ready": + break + time.sleep(60) + counter += 1 + + print("Rerunning Setup...") + res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, + command_name=BLUEPRINT_SETUP_COMMAND) + assert (type(res) is dict) + print("Setup re-run execution response") + pretty_print_response(res) + execution_id = res["executionId"] + return execution_id + + +def test_cancel_setup(admin_session, setup_execution_id): + print("Setup Execution Details") + res = admin_session.get_execution_details(setup_execution_id) + pretty_print_response(res) + print("Ending setup execution") + admin_session.delete_execution(setup_execution_id) + print("Setup execution cancelled") From ea5681340da2496c66391480cdd67656ab7c0028 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 14:35:48 +0200 Subject: [PATCH 17/29] black reformatted --- cloudshell/sandbox_rest/sandbox_api.py | 3 +-- tests/common.py | 3 ++- tests/conftest.py | 11 ++++----- tests/test_api _no_sandbox.py | 8 +++---- tests/test_api_empty_sandbox.py | 32 ++++++++++++-------------- tests/test_api_heath_check_sandbox.py | 21 +++++++---------- tests/test_rerun_setup.py | 14 +++++------ 7 files changed, 40 insertions(+), 52 deletions(-) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 189c9dc..fc5e333 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -403,8 +403,7 @@ def delete_execution(self, execution_id: str) -> None: raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) response_dict = response.json() if not response_dict["result"] == "success": - raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" - f"Response: {response_dict}") + raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}") if __name__ == "__main__": diff --git a/tests/common.py b/tests/common.py index 0b97e70..962b5ce 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,4 +1,5 @@ import json + from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @@ -9,4 +10,4 @@ def pretty_print_response(dict_response): def get_blueprint_id_from_name(api: SandboxRestApiSession, bp_name: str): res = api.get_blueprint_details(bp_name) - return res["id"] \ No newline at end of file + return res["id"] diff --git a/tests/conftest.py b/tests/conftest.py index a95491e..dadf93a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,17 @@ import time import pytest -from env_settings import * from constants import * +from env_settings import * from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @pytest.fixture(scope="session") def admin_session() -> SandboxRestApiSession: - with SandboxRestApiSession(host=CLOUDSHELL_SERVER, - username=CLOUDSHELL_ADMIN_USER, - password=CLOUDSHELL_ADMIN_PASSWORD, - domain=CLOUDSHELL_DOMAIN) as api: + with SandboxRestApiSession( + host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN + ) as api: yield api time.sleep(2) print("admin session token revoked") @@ -26,5 +25,3 @@ def empty_blueprint(): @pytest.fixture(scope="session") def dut_blueprint(): return DUT_BLUEPRINT - - diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py index f95d6b5..4131083 100644 --- a/tests/test_api _no_sandbox.py +++ b/tests/test_api _no_sandbox.py @@ -9,19 +9,19 @@ def test_get_sandboxes(admin_session: SandboxRestApiSession): res = admin_session.get_sandboxes() - assert(type(res) is list) + assert type(res) is list print(f"Sandbox count found in system: {len(res)}") def test_get_blueprints(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprints() - assert(type(bp_res) is list) + assert type(bp_res) is list print(f"Blueprint count found in system: '{len(bp_res)}'") def test_get_default_blueprint(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT) - assert(type(bp_res) is dict) + assert type(bp_res) is dict bp_name = bp_res["name"] print(f"Pulled details for '{bp_name}'") @@ -29,5 +29,5 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession): def test_get_and_delete_token(admin_session: SandboxRestApiSession): """ get token for admin user """ token_res = admin_session.get_token_for_target_user("admin") - assert(type(token_res) is str) + assert type(token_res) is str print(f"Token response: '{token_res}'") diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py index 93edde5..b490bba 100644 --- a/tests/test_api_empty_sandbox.py +++ b/tests/test_api_empty_sandbox.py @@ -4,24 +4,23 @@ import time import pytest -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession -from constants import * from common import * +from constants import * + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @pytest.fixture(scope="module") def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint): res_id = get_blueprint_id_from_name(admin_session, empty_blueprint) - assert(type(res_id) is str) + assert type(res_id) is str return res_id @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox - start_res = admin_session.start_sandbox( - blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test" - ) + start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -43,9 +42,8 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): counter += 1 print("Rerunning Setup...") - res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, - command_name=BLUEPRINT_SETUP_COMMAND) - assert (type(res) is dict) + res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) + assert type(res) is dict print("Setup re-run execution response") pretty_print_response(res) execution_id = res["executionId"] @@ -53,27 +51,27 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): def test_start_stop(admin_session, sandbox_id): - assert (type(sandbox_id) is str) + assert type(sandbox_id) is str print(f"Sandbox ID: {sandbox_id}") def test_get_sandbox_details(admin_session, sandbox_id): details_res = admin_session.get_sandbox_details(sandbox_id) - assert (type(details_res) is dict) + assert type(details_res) is dict sb_name = details_res["name"] print(f"Pulled details for sandbox '{sb_name}'") def test_get_components(admin_session, sandbox_id): components_res = admin_session.get_sandbox_components(sandbox_id) - assert (type(components_res) is list) + assert type(components_res) is list component_count = len(components_res) print(f"component count found: {component_count}") def test_get_sandbox_commands(admin_session, sandbox_id): commands_res = admin_session.get_sandbox_commands(sandbox_id) - assert (type(commands_res) is list) + assert type(commands_res) is list print(f"Sandbox commands: {[x['name'] for x in commands_res]}") first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"]) print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}") @@ -81,25 +79,25 @@ def test_get_sandbox_commands(admin_session, sandbox_id): def test_get_sandbox_events(admin_session, sandbox_id): activity_res = admin_session.get_sandbox_activity(sandbox_id) - assert (type(activity_res) is dict and "events" in activity_res) + assert type(activity_res) is dict and "events" in activity_res events = activity_res["events"] print(f"activity events count: {len(events)}") def test_get_console_output(admin_session, sandbox_id): output_res = admin_session.get_sandbox_output(sandbox_id) - assert (type(output_res) is dict and "entries" in output_res) + assert type(output_res) is dict and "entries" in output_res entries = output_res["entries"] print(f"Sandbox output entries count: {len(entries)}") def test_get_instructions(admin_session, sandbox_id): instructions_res = admin_session.get_sandbox_instructions(sandbox_id) - assert (type(instructions_res) is str) + assert type(instructions_res) is str print(f"Pulled sandbox instructions: '{instructions_res}'") def test_extend_sandbox(admin_session, sandbox_id): extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M") - assert (type(extend_response) is dict and "remaining_time" in extend_response) + assert type(extend_response) is dict and "remaining_time" in extend_response print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}") diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_heath_check_sandbox.py index 25a47fc..43bded9 100644 --- a/tests/test_api_heath_check_sandbox.py +++ b/tests/test_api_heath_check_sandbox.py @@ -7,24 +7,23 @@ import time import pytest -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession -from constants import * from common import * +from constants import * + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @pytest.fixture(scope="module") def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint): res_id = get_blueprint_id_from_name(admin_session, dut_blueprint) - assert(type(res_id) is str) + assert type(res_id) is str return res_id @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox - start_res = admin_session.start_sandbox( - blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test" - ) + start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -43,10 +42,8 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blue @pytest.fixture(scope="module") def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str): print("Starting DUT Command...") - res = admin_session.run_component_command(sandbox_id=sandbox_id, - component_id=component_id, - command_name=DUT_COMMAND) - assert(type(res) is dict) + res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND) + assert type(res) is dict print("Started execution response") pretty_print_response(res) execution_id = res["executionId"] @@ -56,7 +53,7 @@ def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, componen @pytest.fixture(scope="module") def test_get_execution_details(admin_session, execution_id): res = admin_session.get_execution_details(execution_id) - assert(type(res) is dict) + assert type(res) is dict print("Execution Details") pretty_print_response(res) @@ -66,5 +63,3 @@ def test_delete_execution(admin_session, execution_id, test_get_execution_detail time.sleep(1) admin_session.delete_execution(execution_id) print("Execution deleted") - - diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py index 8827729..12da642 100644 --- a/tests/test_rerun_setup.py +++ b/tests/test_rerun_setup.py @@ -4,17 +4,16 @@ import time import pytest -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession -from constants import * from common import * +from constants import * + +from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint): # start sandbox - start_res = admin_session.start_sandbox( - blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test" - ) + start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -36,9 +35,8 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): counter += 1 print("Rerunning Setup...") - res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, - command_name=BLUEPRINT_SETUP_COMMAND) - assert (type(res) is dict) + res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) + assert type(res) is dict print("Setup re-run execution response") pretty_print_response(res) execution_id = res["executionId"] From 94a53f237fbe23fdb2e62c4191b1565a99af3c00 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 14:38:30 +0200 Subject: [PATCH 18/29] isort fix --- cloudshell/sandbox_rest/sandbox_api.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index fc5e333..35c7484 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -4,7 +4,8 @@ import requests -from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException +from cloudshell.sandbox_rest.exceptions import (SandboxRestAuthException, + SandboxRestException) @dataclass @@ -404,8 +405,3 @@ def delete_execution(self, execution_id: str) -> None: response_dict = response.json() if not response_dict["result"] == "success": raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}") - - -if __name__ == "__main__": - admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="end_users") - user_token = admin_api.get_token_for_target_user("end_user") From c386fcdfb238405a106f93f3b499382d78247b55 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 14:42:20 +0200 Subject: [PATCH 19/29] black formatted --- cloudshell/sandbox_rest/exceptions.py | 8 +- cloudshell/sandbox_rest/sandbox_api.py | 268 ++++++++++++++++++------- setup.py | 2 +- tests/conftest.py | 5 +- tests/test_api _no_sandbox.py | 2 +- tests/test_api_empty_sandbox.py | 17 +- tests/test_api_heath_check_sandbox.py | 16 +- tests/test_rerun_setup.py | 8 +- 8 files changed, 242 insertions(+), 84 deletions(-) diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py index f3bfa21..c4154ff 100644 --- a/cloudshell/sandbox_rest/exceptions.py +++ b/cloudshell/sandbox_rest/exceptions.py @@ -2,7 +2,7 @@ class SandboxRestException(Exception): - """ Base Exception Class inside Rest client class """ + """Base Exception Class inside Rest client class""" def __init__(self, msg: str, response: Response = None): self.msg = msg @@ -11,7 +11,9 @@ def __init__(self, msg: str, response: Response = None): def __str__(self): return self._format_err_msg(self.msg, self.response) - def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str: + def _format_err_msg( + self, custom_err_msg="Failed Api Call", response: Response = None + ) -> str: err_msg = f"Sandbox API Error: {custom_err_msg}" if response: @@ -27,6 +29,6 @@ def _format_response_msg(response: Response): class SandboxRestAuthException(SandboxRestException): - """ Failed auth action """ + """Failed auth action""" pass diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 35c7484..c673ad0 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -4,8 +4,10 @@ import requests -from cloudshell.sandbox_rest.exceptions import (SandboxRestAuthException, - SandboxRestException) +from cloudshell.sandbox_rest.exceptions import ( + SandboxRestAuthException, + SandboxRestException, +) @dataclass @@ -26,9 +28,17 @@ class SandboxRestApiSession: """ def __init__( - self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2" + self, + host: str, + username="", + password="", + token="", + domain="Global", + port=82, + is_https=False, + api_version="v2", ): - """ login to api and store headers for future requests """ + """login to api and store headers for future requests""" _protocol = "https" if is_https else "http" self._base_url = f"{_protocol}://{host}:{port}/api" self._versioned_url = f"{self._base_url}/{api_version}" @@ -37,7 +47,9 @@ def __init__( self._password = password self.domain = domain if self.username and self._password: - self.auth_token = self.get_token_with_credentials(username, password, domain) + self.auth_token = self.get_token_with_credentials( + username, password, domain + ) elif token: self.auth_token = token self._auth_headers = self._build_auth_headers(self.auth_token) @@ -55,11 +67,16 @@ def _build_auth_headers(login_token: str) -> dict: """ interpolate token into auth_headers dict """ - auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"} + auth_headers = { + "Authorization": f"Basic {login_token}", + "Content-Type": "application/json", + } return auth_headers def refresh_auth_from_stored_credentials(self) -> None: - self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) + self.auth_token = self.get_token_with_credentials( + self.username, self._password, self.domain + ) self._auth_headers = self._build_auth_headers(self.auth_token) def invalidate_auth(self) -> None: @@ -68,16 +85,22 @@ def invalidate_auth(self) -> None: def _validate_auth_headers(self) -> None: if not self._auth_headers: - raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session") + raise SandboxRestAuthException( + f"No auth headers currently set for '{self.username}' session" + ) # LOGIN METHODS - def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str: + def get_token_with_credentials( + self, user_name: str, password: str, domain: str + ) -> str: """ Get token from credentials - extraneous quotes stripped off token string """ response = requests.put( url=f"{self._base_url}/login", - data=json.dumps({"username": user_name, "password": password, "domain": domain}), + data=json.dumps( + {"username": user_name, "password": password, "domain": domain} + ), headers={"Content-Type": "application/json"}, ) @@ -93,18 +116,24 @@ def get_token_for_target_user(self, user_name: str) -> str: """ self._validate_auth_headers() response = requests.post( - url=f"{self._base_url}/token", data=json.dumps({"username": user_name}), headers=self._auth_headers + url=f"{self._base_url}/token", + data=json.dumps({"username": user_name}), + headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException(f"Failed to get get token for user {user_name}", response) + raise SandboxRestException( + f"Failed to get get token for user {user_name}", response + ) login_token = response.text[1:-1] return login_token def delete_token(self, token_id: str) -> None: self._validate_auth_headers() - response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers) + response = requests.delete( + url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers + ) if not response.ok: raise SandboxRestException("Failed to delete token", response) @@ -123,7 +152,11 @@ def start_sandbox( """ self._validate_auth_headers() url = f"{self._versioned_url}/blueprints/{blueprint_id}/start" - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] + sandbox_name = ( + sandbox_name + if sandbox_name + else self.get_blueprint_details(blueprint_id)["name"] + ) data_dict = { "duration": duration, @@ -132,7 +165,9 @@ def start_sandbox( "params": [asdict(x) for x in bp_params] if bp_params else [], } - response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) + response = requests.post( + url, headers=self._auth_headers, data=json.dumps(data_dict) + ) if not response.ok: custom_msg = f"Failed to start '{blueprint_id}'. " if 400 <= response.status_code < 500: @@ -143,51 +178,80 @@ def start_sandbox( return response.json() def start_persistent_sandbox( - self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None + self, + blueprint_id: str, + sandbox_name="", + bp_params: List[InputParam] = None, + permitted_users: List[str] = None, ) -> dict: - """ Create a persistent sandbox from the provided blueprint id """ + """Create a persistent sandbox from the provided blueprint id""" self._validate_auth_headers() url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" - sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] + sandbox_name = ( + sandbox_name + if sandbox_name + else self.get_blueprint_details(blueprint_id)["name"] + ) data_dict = { "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], "params": [asdict(x) for x in bp_params] if bp_params else [], } - response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) + response = requests.post( + url, headers=self._auth_headers, data=json.dumps(data_dict) + ) if not response.ok: - err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'" + err_msg = ( + f"Failed to start persistent sandbox from blueprint '{blueprint_id}'" + ) raise SandboxRestException(err_msg, response) return response.json() def run_sandbox_command( - self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True + self, + sandbox_id: str, + command_name: str, + params: List[InputParam] = None, + print_output=True, ) -> dict: - """ Run a sandbox level command """ + """Run a sandbox level command""" self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) + response = requests.post( + url, data=json.dumps(data_dict), headers=self._auth_headers + ) if not response.ok: - raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response) + raise SandboxRestException( + f"failed to start sandbox command '{command_name}'", response + ) return response.json() def run_component_command( - self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True + self, + sandbox_id: str, + component_id: str, + command_name: str, + params: List[InputParam] = None, + print_output: bool = True, ) -> dict: - """ Start a command on sandbox component """ + """Start a command on sandbox component""" self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) + response = requests.post( + url, data=json.dumps(data_dict), headers=self._auth_headers + ) if not response.ok: - raise SandboxRestException(f"failed to start component command '{command_name}'", response) + raise SandboxRestException( + f"failed to start component command '{command_name}'", response + ) return response.json() def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: @@ -199,22 +263,31 @@ def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: self._validate_auth_headers() data_dict = {"extended_time": duration} response = requests.post( - f"{self._versioned_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/extend", + data=json.dumps(data_dict), + headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response) + raise SandboxRestException( + f"failed to extend sandbox '{sandbox_id}'", response + ) return response.json() def stop_sandbox(self, sandbox_id: str) -> None: - """ Stop the sandbox given sandbox id """ + """Stop the sandbox given sandbox id""" self._validate_auth_headers() - response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers) + response = requests.post( + f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", + headers=self._auth_headers, + ) if not response.ok: - raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response) + raise SandboxRestException( + f"Failed to stop sandbox '{sandbox_id}'", response + ) # SANDBOX GET REQUESTS def get_sandboxes(self, show_historic=False) -> list: - """ Get list of sandboxes """ + """Get list of sandboxes""" self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes" params = {"show_historic": "true" if show_historic else "false"} @@ -225,7 +298,7 @@ def get_sandboxes(self, show_historic=False) -> list: return response.json() def get_sandbox_details(self, sandbox_id: str) -> dict: - """ Get details of the given sandbox id """ + """Get details of the given sandbox id""" self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}" response = requests.get(url, headers=self._auth_headers) @@ -235,7 +308,12 @@ def get_sandbox_details(self, sandbox_id: str) -> dict: return response.json() def get_sandbox_activity( - self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None + self, + sandbox_id: str, + error_only=False, + since="", + from_event_id: int = None, + tail: int = None, ) -> dict: """ Get list of sandbox activity @@ -263,65 +341,88 @@ def get_sandbox_activity( response = requests.get(url, headers=self._auth_headers) if not response.ok: - raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response) + raise SandboxRestException( + f"Failed to get sandbox activity for '{sandbox_id}'", response + ) return response.json() def get_sandbox_commands(self, sandbox_id: str) -> list: - """ Get list of sandbox commands """ + """Get list of sandbox commands""" self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", + headers=self._auth_headers, + ) if not response.ok: - raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response) + raise SandboxRestException( + f"Failed to get sandbox commands for '{sandbox_id}'", response + ) return response.json() def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict: - """ Get details of specific sandbox command """ + """Get details of specific sandbox command""" self._validate_auth_headers() response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", + headers=self._auth_headers, ) if not response.ok: - err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'" + err_msg = ( + f"Failed to get command details for '{command_name}' in '{sandbox_id}'" + ) raise SandboxRestException(err_msg, response) return response.json() def get_sandbox_components(self, sandbox_id: str) -> list: - """ Get list of sandbox components """ + """Get list of sandbox components""" self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/components", + headers=self._auth_headers, + ) if not response.ok: err_msg = (response, f"Failed to get components for '{sandbox_id}'") raise SandboxRestException(err_msg, response) return response.json() def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> dict: - """ Get details of components in sandbox """ + """Get details of components in sandbox""" self._validate_auth_headers() response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", + headers=self._auth_headers, ) if not response.ok: custom_err_msg = ( - f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" + f"Failed to get sandbox component details.\n" + f"component id: '{component_id}'\n" + f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list: - """ Get list of commands for a particular component in sandbox """ + def get_sandbox_component_commands( + self, sandbox_id: str, component_id: str + ) -> list: + """Get list of commands for a particular component in sandbox""" self._validate_auth_headers() response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers + f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", + headers=self._auth_headers, ) if not response.ok: custom_err_msg = ( - f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" + f"Failed to get component commands.\n" + f"component id: '{component_id}'\n" + f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict: - """ Get details of a command of sandbox component """ + def get_sandbox_component_command_details( + self, sandbox_id: str, component_id: str, command: str + ) -> dict: + """Get details of a command of sandbox component""" self._validate_auth_headers() response = requests.get( f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}", @@ -329,22 +430,33 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s ) if not response.ok: custom_err_msg = ( - f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" + f"Failed to get command details.\n " + f"component id: '{component_id}'\n" + f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() def get_sandbox_instructions(self, sandbox_id: str) -> str: - """ pull the instruction text of sandbox """ + """pull the instruction text of sandbox""" self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", + headers=self._auth_headers, + ) if not response.ok: err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'" raise SandboxRestException(err_msg, response) return response.json() - def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None) -> dict: - """ Get list of sandbox output """ + def get_sandbox_output( + self, + sandbox_id: str, + tail: int = None, + from_entry_id: int = None, + since: str = None, + ) -> dict: + """Get list of sandbox output""" self._validate_auth_headers() url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output" params = {} @@ -367,9 +479,11 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i # BLUEPRINT GET REQUESTS def get_blueprints(self) -> list: - """ Get list of blueprints """ + """Get list of blueprints""" self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/blueprints", headers=self._auth_headers + ) if not response.ok: raise SandboxRestException("Failed to get blueprints", response) return response.json() @@ -380,17 +494,27 @@ def get_blueprint_details(self, blueprint_id: str) -> dict: Can pass either blueprint name OR blueprint ID """ self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/blueprints/{blueprint_id}", headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/blueprints/{blueprint_id}", + headers=self._auth_headers, + ) if not response.ok: - raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response) + raise SandboxRestException( + f"Failed to get blueprint data for '{blueprint_id}'", response + ) return response.json() # EXECUTIONS def get_execution_details(self, execution_id: str) -> dict: self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) + response = requests.get( + f"{self._versioned_url}/executions/{execution_id}", + headers=self._auth_headers, + ) if not response.ok: - raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response) + raise SandboxRestException( + f"Failed to get execution details for '{execution_id}'", response + ) return response.json() def delete_execution(self, execution_id: str) -> None: @@ -399,9 +523,17 @@ def delete_execution(self, execution_id: str) -> None: {"result": "success"} """ self._validate_auth_headers() - response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers) + response = requests.delete( + f"{self._versioned_url}/executions/{execution_id}", + headers=self._auth_headers, + ) if not response.ok: - raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) + raise SandboxRestException( + f"Failed to delete execution for '{execution_id}'", response + ) response_dict = response.json() if not response_dict["result"] == "success": - raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}") + raise SandboxRestException( + f"Failed execution deletion of id {execution_id}\n" + f"Response: {response_dict}" + ) diff --git a/setup.py b/setup.py index dfb0c59..84ba2e5 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ def lines_from_file(file_name: str) -> List[str]: packages=find_packages(), version=read_file("version.txt"), long_description=read_file("README.MD"), - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", install_requires=lines_from_file("requirements.txt"), test_requires=lines_from_file("test-requirements.txt"), classifiers=[ diff --git a/tests/conftest.py b/tests/conftest.py index dadf93a..99d0c61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,10 @@ @pytest.fixture(scope="session") def admin_session() -> SandboxRestApiSession: with SandboxRestApiSession( - host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN + host=CLOUDSHELL_SERVER, + username=CLOUDSHELL_ADMIN_USER, + password=CLOUDSHELL_ADMIN_PASSWORD, + domain=CLOUDSHELL_DOMAIN, ) as api: yield api time.sleep(2) diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py index 4131083..048c9cf 100644 --- a/tests/test_api _no_sandbox.py +++ b/tests/test_api _no_sandbox.py @@ -27,7 +27,7 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession): def test_get_and_delete_token(admin_session: SandboxRestApiSession): - """ get token for admin user """ + """get token for admin user""" token_res = admin_session.get_token_for_target_user("admin") assert type(token_res) is str print(f"Token response: '{token_res}'") diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py index b490bba..c367b75 100644 --- a/tests/test_api_empty_sandbox.py +++ b/tests/test_api_empty_sandbox.py @@ -20,7 +20,9 @@ def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint): @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox - start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test") + start_res = admin_session.start_sandbox( + blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test" + ) sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -42,7 +44,9 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): counter += 1 print("Rerunning Setup...") - res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) + res = admin_session.run_sandbox_command( + sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND + ) assert type(res) is dict print("Setup re-run execution response") pretty_print_response(res) @@ -73,8 +77,13 @@ def test_get_sandbox_commands(admin_session, sandbox_id): commands_res = admin_session.get_sandbox_commands(sandbox_id) assert type(commands_res) is list print(f"Sandbox commands: {[x['name'] for x in commands_res]}") - first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"]) - print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}") + first_sb_command = admin_session.get_sandbox_command_details( + sandbox_id, commands_res[0]["name"] + ) + print( + f"SB command name: {first_sb_command['name']}\n" + f"description: {first_sb_command['description']}" + ) def test_get_sandbox_events(admin_session, sandbox_id): diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_heath_check_sandbox.py index 43bded9..61882d7 100644 --- a/tests/test_api_heath_check_sandbox.py +++ b/tests/test_api_heath_check_sandbox.py @@ -23,7 +23,9 @@ def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint): @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox - start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test") + start_res = admin_session.start_sandbox( + blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test" + ) sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -32,7 +34,9 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): @pytest.fixture(scope="module") -def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str): +def component_id( + admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str +): components = admin_session.get_sandbox_components(sandbox_id) component_filter = [x for x in components if x["component_type"] == DUT_MODEL] assert component_filter @@ -40,9 +44,13 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blue @pytest.fixture(scope="module") -def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str): +def execution_id( + admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str +): print("Starting DUT Command...") - res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND) + res = admin_session.run_component_command( + sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND + ) assert type(res) is dict print("Started execution response") pretty_print_response(res) diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py index 12da642..c546269 100644 --- a/tests/test_rerun_setup.py +++ b/tests/test_rerun_setup.py @@ -13,7 +13,9 @@ @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint): # start sandbox - start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test") + start_res = admin_session.start_sandbox( + blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test" + ) sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -35,7 +37,9 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): counter += 1 print("Rerunning Setup...") - res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) + res = admin_session.run_sandbox_command( + sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND + ) assert type(res) is dict print("Setup re-run execution response") pretty_print_response(res) From 8baa18e477371107f6d7f5dd50215dfde2c2e45c Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 15:55:07 +0200 Subject: [PATCH 20/29] all linting passed! --- .pre-commit-config.yaml | 30 ++-- cloudshell/sandbox_rest/exceptions.py | 16 +- cloudshell/sandbox_rest/sandbox_api.py | 140 +++++------------- ...eck_sandbox.py => test_api_dut_sandbox.py} | 30 ++-- tests/test_api_empty_sandbox.py | 39 ++--- ... _no_sandbox.py => test_api_no_sandbox.py} | 9 +- tests/test_rerun_setup.py | 12 +- 7 files changed, 92 insertions(+), 184 deletions(-) rename tests/{test_api_heath_check_sandbox.py => test_api_dut_sandbox.py} (75%) rename tests/{test_api _no_sandbox.py => test_api_no_sandbox.py} (85%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d05e301..b8d53d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,28 +4,28 @@ repos: hooks: - id: isort language_version: python3.7 - args: [--line-length=127] + args: [ --line-length=127 ] - repo: https://github.com/python/black rev: 20.8b1 hooks: - id: black language_version: python3.7 - args: [--line-length=127] + args: [ --line-length=127 ] - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.3 hooks: - id: flake8 additional_dependencies: [ - flake8-docstrings, - flake8-builtins, - flake8-comprehensions, - flake8-print, - flake8-eradicate, + flake8-docstrings, + flake8-builtins, + flake8-comprehensions, + flake8-print, + flake8-eradicate, ] language_version: python3.7 args: [ - --max-line-length=127, - '--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D210,D401,W503,E203' + --max-line-length=127, + '--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D205,D210,D400,D401,W503,E203,F403,F405,T001' ] # See https://stackoverflow.com/questions/61238318/pylint-and-pre-commit-hook-unable-to-import/61238571#61238571 - repo: local @@ -34,11 +34,11 @@ repos: name: pylint entry: pylint language: system - types: [python] + types: [ python ] args: [ -# --max-line-length=127, -# --max-public-methods=32, -# --max-args=8, - '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding', - '--good-names=ip,rc,eval' + --max-line-length=127, + --max-public-methods=32, + --max-args=9, + '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding, unused-wildcard-import,missing-function-docstring,missing-module-docstring,import-error,wildcard-import,invalid-name,redefined-outer-name,no-name-in-module', + '--good-names=ip,rc,eval' ] \ No newline at end of file diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py index c4154ff..d1f8359 100644 --- a/cloudshell/sandbox_rest/exceptions.py +++ b/cloudshell/sandbox_rest/exceptions.py @@ -2,22 +2,22 @@ class SandboxRestException(Exception): - """Base Exception Class inside Rest client class""" + """ Base Exception Class inside Rest client class """ - def __init__(self, msg: str, response: Response = None): - self.msg = msg + def __init__(self, message: str, response: Response = None): + super().__init__(message) + self.message = message self.response = response def __str__(self): - return self._format_err_msg(self.msg, self.response) + return self._format_err_msg(self.message, self.response) - def _format_err_msg( - self, custom_err_msg="Failed Api Call", response: Response = None - ) -> str: + def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str: err_msg = f"Sandbox API Error: {custom_err_msg}" if response: err_msg += f"\n{self._format_response_msg(response)}" + return err_msg @staticmethod def _format_response_msg(response: Response): @@ -30,5 +30,3 @@ def _format_response_msg(response: Response): class SandboxRestAuthException(SandboxRestException): """Failed auth action""" - - pass diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index c673ad0..35e2588 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -4,10 +4,7 @@ import requests -from cloudshell.sandbox_rest.exceptions import ( - SandboxRestAuthException, - SandboxRestException, -) +from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException @dataclass @@ -38,7 +35,7 @@ def __init__( is_https=False, api_version="v2", ): - """login to api and store headers for future requests""" + """ Login to api and store headers for future requests """ _protocol = "https" if is_https else "http" self._base_url = f"{_protocol}://{host}:{port}/api" self._versioned_url = f"{self._base_url}/{api_version}" @@ -47,9 +44,7 @@ def __init__( self._password = password self.domain = domain if self.username and self._password: - self.auth_token = self.get_token_with_credentials( - username, password, domain - ) + self.auth_token = self.get_token_with_credentials(username, password, domain) elif token: self.auth_token = token self._auth_headers = self._build_auth_headers(self.auth_token) @@ -65,7 +60,7 @@ def __exit__(self, exc_type, exc_value, traceback): @staticmethod def _build_auth_headers(login_token: str) -> dict: """ - interpolate token into auth_headers dict + Interpolate token into auth_headers dict """ auth_headers = { "Authorization": f"Basic {login_token}", @@ -74,9 +69,7 @@ def _build_auth_headers(login_token: str) -> dict: return auth_headers def refresh_auth_from_stored_credentials(self) -> None: - self.auth_token = self.get_token_with_credentials( - self.username, self._password, self.domain - ) + self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) self._auth_headers = self._build_auth_headers(self.auth_token) def invalidate_auth(self) -> None: @@ -85,22 +78,16 @@ def invalidate_auth(self) -> None: def _validate_auth_headers(self) -> None: if not self._auth_headers: - raise SandboxRestAuthException( - f"No auth headers currently set for '{self.username}' session" - ) + raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session") # LOGIN METHODS - def get_token_with_credentials( - self, user_name: str, password: str, domain: str - ) -> str: + def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str: """ Get token from credentials - extraneous quotes stripped off token string """ response = requests.put( url=f"{self._base_url}/login", - data=json.dumps( - {"username": user_name, "password": password, "domain": domain} - ), + data=json.dumps({"username": user_name, "password": password, "domain": domain}), headers={"Content-Type": "application/json"}, ) @@ -122,18 +109,14 @@ def get_token_for_target_user(self, user_name: str) -> str: ) if not response.ok: - raise SandboxRestException( - f"Failed to get get token for user {user_name}", response - ) + raise SandboxRestException(f"Failed to get get token for user {user_name}", response) login_token = response.text[1:-1] return login_token def delete_token(self, token_id: str) -> None: self._validate_auth_headers() - response = requests.delete( - url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers - ) + response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers) if not response.ok: raise SandboxRestException("Failed to delete token", response) @@ -152,11 +135,7 @@ def start_sandbox( """ self._validate_auth_headers() url = f"{self._versioned_url}/blueprints/{blueprint_id}/start" - sandbox_name = ( - sandbox_name - if sandbox_name - else self.get_blueprint_details(blueprint_id)["name"] - ) + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] data_dict = { "duration": duration, @@ -165,9 +144,7 @@ def start_sandbox( "params": [asdict(x) for x in bp_params] if bp_params else [], } - response = requests.post( - url, headers=self._auth_headers, data=json.dumps(data_dict) - ) + response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: custom_msg = f"Failed to start '{blueprint_id}'. " if 400 <= response.status_code < 500: @@ -188,24 +165,16 @@ def start_persistent_sandbox( self._validate_auth_headers() url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" - sandbox_name = ( - sandbox_name - if sandbox_name - else self.get_blueprint_details(blueprint_id)["name"] - ) + sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] data_dict = { "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], "params": [asdict(x) for x in bp_params] if bp_params else [], } - response = requests.post( - url, headers=self._auth_headers, data=json.dumps(data_dict) - ) + response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) if not response.ok: - err_msg = ( - f"Failed to start persistent sandbox from blueprint '{blueprint_id}'" - ) + err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'" raise SandboxRestException(err_msg, response) return response.json() @@ -222,13 +191,9 @@ def run_sandbox_command( data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params - response = requests.post( - url, data=json.dumps(data_dict), headers=self._auth_headers - ) + response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - f"failed to start sandbox command '{command_name}'", response - ) + raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response) return response.json() def run_component_command( @@ -245,13 +210,9 @@ def run_component_command( data_dict = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] data_dict["params"] = params - response = requests.post( - url, data=json.dumps(data_dict), headers=self._auth_headers - ) + response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - f"failed to start component command '{command_name}'", response - ) + raise SandboxRestException(f"failed to start component command '{command_name}'", response) return response.json() def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: @@ -268,9 +229,7 @@ def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException( - f"failed to extend sandbox '{sandbox_id}'", response - ) + raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response) return response.json() def stop_sandbox(self, sandbox_id: str) -> None: @@ -281,9 +240,7 @@ def stop_sandbox(self, sandbox_id: str) -> None: headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException( - f"Failed to stop sandbox '{sandbox_id}'", response - ) + raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response) # SANDBOX GET REQUESTS def get_sandboxes(self, show_historic=False) -> list: @@ -341,9 +298,7 @@ def get_sandbox_activity( response = requests.get(url, headers=self._auth_headers) if not response.ok: - raise SandboxRestException( - f"Failed to get sandbox activity for '{sandbox_id}'", response - ) + raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response) return response.json() def get_sandbox_commands(self, sandbox_id: str) -> list: @@ -354,9 +309,7 @@ def get_sandbox_commands(self, sandbox_id: str) -> list: headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException( - f"Failed to get sandbox commands for '{sandbox_id}'", response - ) + raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response) return response.json() def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict: @@ -367,9 +320,7 @@ def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dic headers=self._auth_headers, ) if not response.ok: - err_msg = ( - f"Failed to get command details for '{command_name}' in '{sandbox_id}'" - ) + err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'" raise SandboxRestException(err_msg, response) return response.json() @@ -394,16 +345,12 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> d ) if not response.ok: custom_err_msg = ( - f"Failed to get sandbox component details.\n" - f"component id: '{component_id}'\n" - f"sandbox id: '{sandbox_id}'" + f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_component_commands( - self, sandbox_id: str, component_id: str - ) -> list: + def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list: """Get list of commands for a particular component in sandbox""" self._validate_auth_headers() response = requests.get( @@ -412,16 +359,12 @@ def get_sandbox_component_commands( ) if not response.ok: custom_err_msg = ( - f"Failed to get component commands.\n" - f"component id: '{component_id}'\n" - f"sandbox id: '{sandbox_id}'" + f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() - def get_sandbox_component_command_details( - self, sandbox_id: str, component_id: str, command: str - ) -> dict: + def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict: """Get details of a command of sandbox component""" self._validate_auth_headers() response = requests.get( @@ -430,15 +373,13 @@ def get_sandbox_component_command_details( ) if not response.ok: custom_err_msg = ( - f"Failed to get command details.\n " - f"component id: '{component_id}'\n" - f"sandbox id: '{sandbox_id}'" + f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" ) raise SandboxRestException(custom_err_msg, response) return response.json() def get_sandbox_instructions(self, sandbox_id: str) -> str: - """pull the instruction text of sandbox""" + """ Pull the instructions text of sandbox """ self._validate_auth_headers() response = requests.get( f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", @@ -481,9 +422,7 @@ def get_sandbox_output( def get_blueprints(self) -> list: """Get list of blueprints""" self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/blueprints", headers=self._auth_headers - ) + response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) if not response.ok: raise SandboxRestException("Failed to get blueprints", response) return response.json() @@ -499,9 +438,7 @@ def get_blueprint_details(self, blueprint_id: str) -> dict: headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException( - f"Failed to get blueprint data for '{blueprint_id}'", response - ) + raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response) return response.json() # EXECUTIONS @@ -512,9 +449,7 @@ def get_execution_details(self, execution_id: str) -> dict: headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException( - f"Failed to get execution details for '{execution_id}'", response - ) + raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response) return response.json() def delete_execution(self, execution_id: str) -> None: @@ -528,12 +463,7 @@ def delete_execution(self, execution_id: str) -> None: headers=self._auth_headers, ) if not response.ok: - raise SandboxRestException( - f"Failed to delete execution for '{execution_id}'", response - ) + raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) response_dict = response.json() if not response_dict["result"] == "success": - raise SandboxRestException( - f"Failed execution deletion of id {execution_id}\n" - f"Response: {response_dict}" - ) + raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}") diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_dut_sandbox.py similarity index 75% rename from tests/test_api_heath_check_sandbox.py rename to tests/test_api_dut_sandbox.py index 61882d7..7f9d61e 100644 --- a/tests/test_api_heath_check_sandbox.py +++ b/tests/test_api_dut_sandbox.py @@ -1,5 +1,6 @@ """ -Test the api methods against blueprint with a resource containing a command +Test the api methods against blueprint with a resource containing a command. + - Putshell mock can be used - https://community.quali.com/repos/3318/put-shell-mock - DUT model / command can be referenced in constants.py (Putshell / health_check) - Assumed that only one DUT is in blueprint @@ -16,16 +17,14 @@ @pytest.fixture(scope="module") def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint): res_id = get_blueprint_id_from_name(admin_session, dut_blueprint) - assert type(res_id) is str + assert isinstance(res_id, str) return res_id @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox - start_res = admin_session.start_sandbox( - blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test" - ) + start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -34,9 +33,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): @pytest.fixture(scope="module") -def component_id( - admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str -): +def component_id(admin_session: SandboxRestApiSession, sandbox_id: str): components = admin_session.get_sandbox_components(sandbox_id) component_filter = [x for x in components if x["component_type"] == DUT_MODEL] assert component_filter @@ -44,14 +41,10 @@ def component_id( @pytest.fixture(scope="module") -def execution_id( - admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str -): +def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str): print("Starting DUT Command...") - res = admin_session.run_component_command( - sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND - ) - assert type(res) is dict + res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND) + assert isinstance(res, dict) print("Started execution response") pretty_print_response(res) execution_id = res["executionId"] @@ -61,12 +54,13 @@ def execution_id( @pytest.fixture(scope="module") def test_get_execution_details(admin_session, execution_id): res = admin_session.get_execution_details(execution_id) - assert type(res) is dict - print("Execution Details") - pretty_print_response(res) + assert isinstance(res, dict) + return res def test_delete_execution(admin_session, execution_id, test_get_execution_details): + print("Execution Details") + pretty_print_response(test_get_execution_details) print("Stopping execution...") time.sleep(1) admin_session.delete_execution(execution_id) diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py index c367b75..e326f0b 100644 --- a/tests/test_api_empty_sandbox.py +++ b/tests/test_api_empty_sandbox.py @@ -13,16 +13,14 @@ @pytest.fixture(scope="module") def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint): res_id = get_blueprint_id_from_name(admin_session, empty_blueprint) - assert type(res_id) is str + assert isinstance(res_id, str) return res_id @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): # start sandbox - start_res = admin_session.start_sandbox( - blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test" - ) + start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -44,69 +42,62 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): counter += 1 print("Rerunning Setup...") - res = admin_session.run_sandbox_command( - sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND - ) - assert type(res) is dict + res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) + assert isinstance(res, dict) print("Setup re-run execution response") pretty_print_response(res) execution_id = res["executionId"] return execution_id -def test_start_stop(admin_session, sandbox_id): - assert type(sandbox_id) is str +def test_start_stop(sandbox_id): + assert isinstance(sandbox_id, str) print(f"Sandbox ID: {sandbox_id}") def test_get_sandbox_details(admin_session, sandbox_id): details_res = admin_session.get_sandbox_details(sandbox_id) - assert type(details_res) is dict + assert isinstance(details_res, dict) sb_name = details_res["name"] print(f"Pulled details for sandbox '{sb_name}'") def test_get_components(admin_session, sandbox_id): components_res = admin_session.get_sandbox_components(sandbox_id) - assert type(components_res) is list + assert isinstance(components_res, list) component_count = len(components_res) print(f"component count found: {component_count}") def test_get_sandbox_commands(admin_session, sandbox_id): commands_res = admin_session.get_sandbox_commands(sandbox_id) - assert type(commands_res) is list + assert isinstance(commands_res, list) print(f"Sandbox commands: {[x['name'] for x in commands_res]}") - first_sb_command = admin_session.get_sandbox_command_details( - sandbox_id, commands_res[0]["name"] - ) - print( - f"SB command name: {first_sb_command['name']}\n" - f"description: {first_sb_command['description']}" - ) + first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"]) + print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}") def test_get_sandbox_events(admin_session, sandbox_id): activity_res = admin_session.get_sandbox_activity(sandbox_id) - assert type(activity_res) is dict and "events" in activity_res + assert isinstance(activity_res, dict) and "events" in activity_res events = activity_res["events"] print(f"activity events count: {len(events)}") def test_get_console_output(admin_session, sandbox_id): output_res = admin_session.get_sandbox_output(sandbox_id) - assert type(output_res) is dict and "entries" in output_res + assert isinstance(output_res, dict) and "entries" in output_res entries = output_res["entries"] print(f"Sandbox output entries count: {len(entries)}") def test_get_instructions(admin_session, sandbox_id): instructions_res = admin_session.get_sandbox_instructions(sandbox_id) - assert type(instructions_res) is str + assert isinstance(instructions_res, str) print(f"Pulled sandbox instructions: '{instructions_res}'") def test_extend_sandbox(admin_session, sandbox_id): extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M") - assert type(extend_response) is dict and "remaining_time" in extend_response + assert isinstance(extend_response, dict) and "remaining_time" in extend_response print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}") diff --git a/tests/test_api _no_sandbox.py b/tests/test_api_no_sandbox.py similarity index 85% rename from tests/test_api _no_sandbox.py rename to tests/test_api_no_sandbox.py index 048c9cf..f586407 100644 --- a/tests/test_api _no_sandbox.py +++ b/tests/test_api_no_sandbox.py @@ -9,25 +9,24 @@ def test_get_sandboxes(admin_session: SandboxRestApiSession): res = admin_session.get_sandboxes() - assert type(res) is list + assert isinstance(res, list) print(f"Sandbox count found in system: {len(res)}") def test_get_blueprints(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprints() - assert type(bp_res) is list + assert isinstance(bp_res, list) print(f"Blueprint count found in system: '{len(bp_res)}'") def test_get_default_blueprint(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT) - assert type(bp_res) is dict + assert isinstance(bp_res, dict) bp_name = bp_res["name"] print(f"Pulled details for '{bp_name}'") def test_get_and_delete_token(admin_session: SandboxRestApiSession): - """get token for admin user""" token_res = admin_session.get_token_for_target_user("admin") - assert type(token_res) is str + assert isinstance(token_res, str) print(f"Token response: '{token_res}'") diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py index c546269..1ae31b1 100644 --- a/tests/test_rerun_setup.py +++ b/tests/test_rerun_setup.py @@ -1,5 +1,5 @@ """ -Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution, finally end sandbox +Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution """ import time @@ -13,9 +13,7 @@ @pytest.fixture(scope="module") def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint): # start sandbox - start_res = admin_session.start_sandbox( - blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test" - ) + start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") yield sandbox_id @@ -37,10 +35,8 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): counter += 1 print("Rerunning Setup...") - res = admin_session.run_sandbox_command( - sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND - ) - assert type(res) is dict + res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) + assert isinstance(res, dict) print("Setup re-run execution response") pretty_print_response(res) execution_id = res["executionId"] From 5755e95aa9de97f62219f82d2425fc4dfa71fcd8 Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Sat, 27 Nov 2021 16:04:27 +0200 Subject: [PATCH 21/29] Delete cloudshell/__pycache__ directory --- cloudshell/__pycache__/__init__.cpython-37.pyc | Bin 270 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cloudshell/__pycache__/__init__.cpython-37.pyc diff --git a/cloudshell/__pycache__/__init__.cpython-37.pyc b/cloudshell/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 1638dc996911c33a01b65615684ced948aabc95d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270 zcmXwyO-chn5QV#YCK{r-g1DVUa)2P>&&rJi1~)@X(M}gmo1X5me;`?V8m}~47vdFM zSsCy_ef5fZ@a~q&1tV>~pOvEhbv*u6kh!6W7X-~3rnwfD_o5M;p}+#m57895s99Au zv-hm5`JpJQ_;`lz!=9Mme>~-o*BD2>uP*6lJZPe)0R&?e<}{Jy4sIt@T;Q4y}~{ Z#+d}L9uDLDs`oluyynysOG3%d`5y=LOV|Ja From 7ec339860b494141151860d15fdf38ef837bfbca Mon Sep 17 00:00:00 2001 From: Natti Katz Date: Sat, 27 Nov 2021 16:05:02 +0200 Subject: [PATCH 22/29] Delete cloudshell/sandbox_rest/__pycache__ directory --- .../__pycache__/__init__.cpython-37.pyc | Bin 283 -> 0 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 287 -> 0 bytes .../__pycache__/sandbox_api.cpython-37.pyc | Bin 17475 -> 0 bytes .../__pycache__/sandbox_api.cpython-39.pyc | Bin 13066 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc delete mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc delete mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index eee6736ad4ff2f82925fd57e10453058b115b48f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283 zcmXv{yG{c!5VU<4aSF*VAR3A*k`F)#AwGbH?qq2(=T*+cXB$7Hq~>e*CAU=k0u`G` z8EI!{cSf4$^?Jz&t2?cb_Rs0Kpds-@lWvGK8<^%=SU!tEaE1aKY`?_N=;oF6ZC!j8 zWz8=|STi7T3TN~BrCG}prNUNnL;6j)&SE}B9Y8&;Qj z_BJYOzA4ITo{w>t&}kS{>h9`Uo&%^cbv^*(qhb3pCS${)AXhMj=uekZJpz2PP?0s! z&(qDhypI^7bSfpY+RD~zlvA7RO>EDG;9s##QErse&wjVOUf(X)dunsCwSLsGLu+OH fb0J_%62Lf<09M03R~Nn4qs2>3rLiO``3e68{Qpuf diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc deleted file mode 100644 index 0c82e772dd292885b28a57a3cf3496288ebef2a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17475 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 a#(suf?LW(nl-ZloIq^?B_~=`kqVhipt1#OD diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc deleted file mode 100644 index 2eef5552e21c5b945ca34ec9e8843b988fe1aa3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13066 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 From b096ac959a32fc81a46c9845e2a54fecee083b86 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 16:05:33 +0200 Subject: [PATCH 23/29] modify git ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9ae8ac3..6e01c40 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ *.egg-info tests/.env *.zip -dist/ \ No newline at end of file +dist/ +*.pyc \ No newline at end of file From 51a8512077076bc853123e230f07dcc5538ba5ab Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 16:11:59 +0200 Subject: [PATCH 24/29] black formatted init --- cloudshell/sandbox_rest/sandbox_api.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index 35e2588..eb671a7 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -25,15 +25,7 @@ class SandboxRestApiSession: """ def __init__( - self, - host: str, - username="", - password="", - token="", - domain="Global", - port=82, - is_https=False, - api_version="v2", + self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2" ): """ Login to api and store headers for future requests """ _protocol = "https" if is_https else "http" From b028c410b5881a62ee07d234ff276c037e19a460 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 16:21:57 +0200 Subject: [PATCH 25/29] added utility token refresh method --- cloudshell/sandbox_rest/sandbox_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py index eb671a7..f87d619 100644 --- a/cloudshell/sandbox_rest/sandbox_api.py +++ b/cloudshell/sandbox_rest/sandbox_api.py @@ -64,6 +64,10 @@ def refresh_auth_from_stored_credentials(self) -> None: self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) self._auth_headers = self._build_auth_headers(self.auth_token) + def refresh_auth_from_token(self, token: str) -> None: + self.auth_token = token + self._auth_headers = self._build_auth_headers(token) + def invalidate_auth(self) -> None: self.delete_token(self.auth_token) self._auth_headers = None From 675b9b78815d22435257a473b57d4a9c79f3a4d9 Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sat, 27 Nov 2021 17:59:28 +0200 Subject: [PATCH 26/29] added new test --- .gitignore | 4 +++- LICENSE | 22 ++++++++++++++++++++++ test-requirements.txt | 3 ++- tests/test_api_no_sandbox.py | 30 ++++++++++++++++++++++++------ 4 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore index 6e01c40..96702e8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ tests/.env *.zip dist/ -*.pyc \ No newline at end of file +build/ +*.pyc +venv \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93ee38a --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + +MIT License + +Copyright (c) 2021 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt index 2ed62ba..ff6f3e3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,5 @@ pytest python-dotenv flake8 -pylint \ No newline at end of file +pylint +pre-commit \ No newline at end of file diff --git a/tests/test_api_no_sandbox.py b/tests/test_api_no_sandbox.py index f586407..f12c19b 100644 --- a/tests/test_api_no_sandbox.py +++ b/tests/test_api_no_sandbox.py @@ -2,11 +2,35 @@ Test the api methods that do NOT require a blueprint Live Cloudshell server is still a dependency """ +import pytest from constants import DEFAULT_EMPTY_BLUEPRINT from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +@pytest.fixture(scope="module") +def api_token(admin_session: SandboxRestApiSession): + token = admin_session.get_token_for_target_user("admin") + return token + + +def test_delete_token(admin_session: SandboxRestApiSession, api_token: str): + assert isinstance(api_token, str) + print(f"Token response: '{api_token}'") + admin_session.delete_token(api_token) + + +def test_reset_session(admin_session: SandboxRestApiSession): + print("\ndeleting admin token...") + admin_session.invalidate_auth() + print("logging back in..") + admin_session.refresh_auth_from_stored_credentials() + print("Calling get blueprints...") + res = admin_session.get_blueprints() + assert isinstance(res, list) + print(f"blueprint count {len(res)}") + + def test_get_sandboxes(admin_session: SandboxRestApiSession): res = admin_session.get_sandboxes() assert isinstance(res, list) @@ -24,9 +48,3 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession): assert isinstance(bp_res, dict) bp_name = bp_res["name"] print(f"Pulled details for '{bp_name}'") - - -def test_get_and_delete_token(admin_session: SandboxRestApiSession): - token_res = admin_session.get_token_for_target_user("admin") - assert isinstance(token_res, str) - print(f"Token response: '{token_res}'") From c79e90d1103bfd15da7590d8b8523ac5a40bc0dd Mon Sep 17 00:00:00 2001 From: katzy687 Date: Sun, 28 Nov 2021 04:53:43 +0200 Subject: [PATCH 27/29] refactored project files --- pyproject.toml | 3 ++ test-requirements.txt => requirements-dev.txt | 0 requirements.txt | 3 +- setup.cfg | 27 ++++++++++ setup.py | 40 ++------------ {cloudshell => src/cloudshell}/__init__.py | 0 .../cloudshell}/sandbox_rest/__init__.py | 0 .../cloudshell}/sandbox_rest/exceptions.py | 0 .../cloudshell}/sandbox_rest/sandbox_api.py | 0 tests/common.py | 2 +- tests/test_rerun_setup.py | 52 ------------------- 11 files changed, 35 insertions(+), 92 deletions(-) create mode 100644 pyproject.toml rename test-requirements.txt => requirements-dev.txt (100%) create mode 100644 setup.cfg rename {cloudshell => src/cloudshell}/__init__.py (100%) rename {cloudshell => src/cloudshell}/sandbox_rest/__init__.py (100%) rename {cloudshell => src/cloudshell}/sandbox_rest/exceptions.py (100%) rename {cloudshell => src/cloudshell}/sandbox_rest/sandbox_api.py (100%) delete mode 100644 tests/test_rerun_setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1b68d94 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/test-requirements.txt b/requirements-dev.txt similarity index 100% rename from test-requirements.txt rename to requirements-dev.txt diff --git a/requirements.txt b/requirements.txt index e886ed4..37f34de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -requests -retrying \ No newline at end of file +requests>=2,<3 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..06172dc --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[metadata] +name = cloudshell_sandboxapi_wrapper +version = file: version.txt +author = QualiLab +author_email = support@qualisystems.com +description = Python client for CloudShell Sandbox REST api - consume sandboxes via CI +long_description = file: README.MD +long_description_content_type = text/markdown +url = https://github.com/QualiSystemsLab/Sandbox-API-Python +classifiers = + Programming Language :: Python :: 3.7 + License :: OSI Approved :: MIT License + Operating System :: OS Independent +license = MIT +license_file = LICENSE + + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.7 +install_requires = + requests>=2 + +[options.packages.find] +where = src \ No newline at end of file diff --git a/setup.py b/setup.py index 84ba2e5..da0fa5e 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,4 @@ -from typing import List +from setuptools import setup -from setuptools import find_packages, setup - - -def read_file(file_name: str) -> str: - with open(file_name) as f: - content = f.read().strip() - return content - - -def lines_from_file(file_name: str) -> List[str]: - with open(file_name) as f: - lines = f.read().splitlines() - return lines - - -setup( - name="cloudshell_sandboxapi_wrapper", - description="Python client for CloudShell Sandbox REST api - consume sandboxes via CI", - keywords=["cloudshell", "sandbox", "api", "rest", "CI"], - url="https://github.com/QualiSystemsLab/Sandbox-API-Python", - author="sadanand.s", - author_email="sadanand.s@quali.com", - license="Apache 2.0", - packages=find_packages(), - version=read_file("version.txt"), - long_description=read_file("README.MD"), - long_description_content_type="text/markdown", - install_requires=lines_from_file("requirements.txt"), - test_requires=lines_from_file("test-requirements.txt"), - classifiers=[ - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - ], -) +if __name__ == "__main__": + setup() diff --git a/cloudshell/__init__.py b/src/cloudshell/__init__.py similarity index 100% rename from cloudshell/__init__.py rename to src/cloudshell/__init__.py diff --git a/cloudshell/sandbox_rest/__init__.py b/src/cloudshell/sandbox_rest/__init__.py similarity index 100% rename from cloudshell/sandbox_rest/__init__.py rename to src/cloudshell/sandbox_rest/__init__.py diff --git a/cloudshell/sandbox_rest/exceptions.py b/src/cloudshell/sandbox_rest/exceptions.py similarity index 100% rename from cloudshell/sandbox_rest/exceptions.py rename to src/cloudshell/sandbox_rest/exceptions.py diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py similarity index 100% rename from cloudshell/sandbox_rest/sandbox_api.py rename to src/cloudshell/sandbox_rest/sandbox_api.py diff --git a/tests/common.py b/tests/common.py index 962b5ce..6b97c3c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,6 +1,6 @@ import json -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession +from src.cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession def pretty_print_response(dict_response): diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py deleted file mode 100644 index 1ae31b1..0000000 --- a/tests/test_rerun_setup.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution -""" -import time - -import pytest -from common import * -from constants import * - -from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession - - -@pytest.fixture(scope="module") -def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint): - # start sandbox - start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test") - sandbox_id = start_res["id"] - print(f"Sandbox started: {sandbox_id}") - yield sandbox_id - admin_session.stop_sandbox(sandbox_id) - print(f"\nSandbox ended: {sandbox_id}") - - -@pytest.fixture(scope="module") -def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): - polling_minutes = 2 - counter = 0 - while True: - if counter > polling_minutes: - raise Exception("Timeout waiting for setup to end") - state = admin_session.get_sandbox_details(sandbox_id)["state"] - if state == "Ready": - break - time.sleep(60) - counter += 1 - - print("Rerunning Setup...") - res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) - assert isinstance(res, dict) - print("Setup re-run execution response") - pretty_print_response(res) - execution_id = res["executionId"] - return execution_id - - -def test_cancel_setup(admin_session, setup_execution_id): - print("Setup Execution Details") - res = admin_session.get_execution_details(setup_execution_id) - pretty_print_response(res) - print("Ending setup execution") - admin_session.delete_execution(setup_execution_id) - print("Setup execution cancelled") From 91736928e55c4f2910a8f19be0b6545f438d96fa Mon Sep 17 00:00:00 2001 From: katzy687 Date: Mon, 6 Dec 2021 21:30:30 +0200 Subject: [PATCH 28/29] tests passing --- .pre-commit-config.yaml | 4 +- requirements.txt | 3 +- src/cloudshell/sandbox_rest/exceptions.py | 28 +- src/cloudshell/sandbox_rest/sandbox_api.py | 392 +++++++-------------- tests/common.py | 13 + tests/conftest.py | 15 +- tests/test_api_dut_sandbox.py | 12 +- tests/test_api_empty_sandbox.py | 31 +- tests/test_api_no_sandbox.py | 15 +- 9 files changed, 181 insertions(+), 332 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8d53d5..f6badf9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: args: [ --max-line-length=127, --max-public-methods=32, - --max-args=9, - '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding, unused-wildcard-import,missing-function-docstring,missing-module-docstring,import-error,wildcard-import,invalid-name,redefined-outer-name,no-name-in-module', + --max-args=13, + '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding, unused-wildcard-import,missing-function-docstring,missing-module-docstring,import-error,wildcard-import,invalid-name,redefined-outer-name,no-name-in-module, arguments-differ', '--good-names=ip,rc,eval' ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 37f34de..9bcc92b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests>=2,<3 \ No newline at end of file +requests>=2,<3 +abstract-http-client \ No newline at end of file diff --git a/src/cloudshell/sandbox_rest/exceptions.py b/src/cloudshell/sandbox_rest/exceptions.py index d1f8359..befbf51 100644 --- a/src/cloudshell/sandbox_rest/exceptions.py +++ b/src/cloudshell/sandbox_rest/exceptions.py @@ -1,32 +1,6 @@ -from requests import Response - - class SandboxRestException(Exception): """ Base Exception Class inside Rest client class """ - def __init__(self, message: str, response: Response = None): - super().__init__(message) - self.message = message - self.response = response - - def __str__(self): - return self._format_err_msg(self.message, self.response) - - def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str: - err_msg = f"Sandbox API Error: {custom_err_msg}" - - if response: - err_msg += f"\n{self._format_response_msg(response)}" - return err_msg - - @staticmethod - def _format_response_msg(response: Response): - return ( - f"Response: {response.status_code}, Reason: {response.reason}\n" - f"Request URL: {response.request.url}\n" - f"Request Headers: {response.request.headers}" - ) - class SandboxRestAuthException(SandboxRestException): - """Failed auth action""" + """ Failed login action """ diff --git a/src/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py index f87d619..ff732e6 100644 --- a/src/cloudshell/sandbox_rest/sandbox_api.py +++ b/src/cloudshell/sandbox_rest/sandbox_api.py @@ -1,8 +1,9 @@ import json +import logging from dataclasses import asdict, dataclass from typing import List -import requests +from abstract_http_client.http_clients.requests_client import RequestsClient from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException @@ -18,103 +19,95 @@ class InputParam: value: str -class SandboxRestApiSession: +class SandboxRestApiSession(RequestsClient): """ Python wrapper for CloudShell Sandbox API View http:///api/v2/explore to see schemas of return json values """ def __init__( - self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2" + self, + host: str, + username="", + password="", + domain="Global", + token="", + port=82, + logger: logging.Logger = None, + use_https=False, + ssl_verify=False, + proxies: dict = None, + insecure_warning=False, ): """ Login to api and store headers for future requests """ - _protocol = "https" if is_https else "http" - self._base_url = f"{_protocol}://{host}:{port}/api" - self._versioned_url = f"{self._base_url}/{api_version}" - self.host = host - self.username = username - self._password = password + super().__init__(host, username, password, token, logger, port, use_https, ssl_verify, proxies, insecure_warning) + self._base_uri = "/api" + self._v2_base_uri = f"{self._base_uri}/v2" self.domain = domain - if self.username and self._password: - self.auth_token = self.get_token_with_credentials(username, password, domain) - elif token: - self.auth_token = token - self._auth_headers = self._build_auth_headers(self.auth_token) - - # CONTEXT MANAGER METHODS - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - # invalidate stored api token on exit - self.invalidate_auth() - - @staticmethod - def _build_auth_headers(login_token: str) -> dict: - """ - Interpolate token into auth_headers dict - """ - auth_headers = { - "Authorization": f"Basic {login_token}", - "Content-Type": "application/json", - } - return auth_headers + self.login() + + def login(self, user="", password="", token="", domain="") -> None: + self.user = user or self.user + self.password = password or self.password + self.token = token or self.token + self.domain = domain or self.domain - def refresh_auth_from_stored_credentials(self) -> None: - self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain) - self._auth_headers = self._build_auth_headers(self.auth_token) + if not self.domain: + raise ValueError("Domain must be passed to login") - def refresh_auth_from_token(self, token: str) -> None: - self.auth_token = token - self._auth_headers = self._build_auth_headers(token) + if not self.token and self.user and self.password: + self.token = self._get_token_with_credentials(self.user, self.password, self.domain) + else: + raise ValueError("Login requires Token or Username / Password") - def invalidate_auth(self) -> None: - self.delete_token(self.auth_token) - self._auth_headers = None + self._set_auth_header_on_session() - def _validate_auth_headers(self) -> None: - if not self._auth_headers: - raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session") + def logout(self) -> None: + if not self.token: + return + self.delete_token(self.token) + self.token = None + self._remove_auth_header_from_session() - # LOGIN METHODS - def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str: + def _get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str: """ Get token from credentials - extraneous quotes stripped off token string """ - response = requests.put( - url=f"{self._base_url}/login", - data=json.dumps({"username": user_name, "password": password, "domain": domain}), - headers={"Content-Type": "application/json"}, - ) - - if not response.ok: - raise SandboxRestAuthException("Failed Login", response) + uri = f"{self._base_uri}/login" + data = {"username": user_name, "password": password, "domain": domain} + response = self.rest_service.request_put(uri, data) login_token = response.text[1:-1] + if not login_token: + raise SandboxRestAuthException(f"Invalid token. Token response {response.text}") + return login_token + def _set_auth_header_on_session(self): + self.rest_service.session.headers.update({"Authorization": f"Basic {self.token}"}) + + def _remove_auth_header_from_session(self): + self.rest_service.session.headers.pop("Authorization") + + def _validate_auth_header(self) -> None: + if not self.rest_service.session.headers.get("Authorization"): + raise SandboxRestAuthException("No Authorization header currently set for session") + def get_token_for_target_user(self, user_name: str) -> str: """ Get token for target user - remove extraneous quotes """ - self._validate_auth_headers() - response = requests.post( - url=f"{self._base_url}/token", - data=json.dumps({"username": user_name}), - headers=self._auth_headers, - ) - - if not response.ok: - raise SandboxRestException(f"Failed to get get token for user {user_name}", response) - + self._validate_auth_header() + uri = f"{self._base_uri}/token" + data = {"username": user_name} + response = self.rest_service.request_post(uri, data) login_token = response.text[1:-1] return login_token def delete_token(self, token_id: str) -> None: - self._validate_auth_headers() - response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException("Failed to delete token", response) + self._validate_auth_header() + uri = f"{self._base_uri}/token/{token_id}" + return self.rest_service.request_delete(uri).text # SANDBOX POST REQUESTS def start_sandbox( @@ -129,26 +122,18 @@ def start_sandbox( Create a sandbox from the provided blueprint id Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M') """ - self._validate_auth_headers() - url = f"{self._versioned_url}/blueprints/{blueprint_id}/start" + self._validate_auth_header() + uri = f"{self._v2_base_uri}/blueprints/{blueprint_id}/start" sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] - data_dict = { + data = { "duration": duration, "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], "params": [asdict(x) for x in bp_params] if bp_params else [], } - response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) - if not response.ok: - custom_msg = f"Failed to start '{blueprint_id}'. " - if 400 <= response.status_code < 500: - custom_msg += "Ensure blueprint is 'Public' and in correct domain." - if response.status_code >= 500: - custom_msg += "Internal Server 500 Error During Request." - raise SandboxRestException(custom_msg, response) - return response.json() + return self.rest_service.request_post(uri, data).json() def start_persistent_sandbox( self, @@ -157,22 +142,18 @@ def start_persistent_sandbox( bp_params: List[InputParam] = None, permitted_users: List[str] = None, ) -> dict: - """Create a persistent sandbox from the provided blueprint id""" - self._validate_auth_headers() - url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent" + """ Create a persistent sandbox from the provided blueprint id """ + self._validate_auth_header() + uri = f"{self._v2_base_uri}/blueprints/{blueprint_id}/start-persistent" sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"] - data_dict = { + data = { "name": sandbox_name, "permitted_users": permitted_users if permitted_users else [], "params": [asdict(x) for x in bp_params] if bp_params else [], } - response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict)) - if not response.ok: - err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'" - raise SandboxRestException(err_msg, response) - return response.json() + return self.rest_service.request_post(uri, data).json() def run_sandbox_command( self, @@ -182,15 +163,12 @@ def run_sandbox_command( print_output=True, ) -> dict: """Run a sandbox level command""" - self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start" - data_dict = {"printOutput": print_output} + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/commands/{command_name}/start" + data = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] - data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response) - return response.json() + data["params"] = params + return self.rest_service.request_post(uri, data).json() def run_component_command( self, @@ -201,15 +179,12 @@ def run_component_command( print_output: bool = True, ) -> dict: """Start a command on sandbox component""" - self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" - data_dict = {"printOutput": print_output} + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start" + data = {"printOutput": print_output} params = [asdict(x) for x in params] if params else [] - data_dict["params"] = params - response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers) - if not response.ok: - raise SandboxRestException(f"failed to start component command '{command_name}'", response) - return response.json() + data["params"] = params + return self.rest_service.request_post(uri, data).json() def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: """Extend the sandbox @@ -217,48 +192,30 @@ def extend_sandbox(self, sandbox_id: str, duration: str) -> dict: :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec) :return: """ - self._validate_auth_headers() - data_dict = {"extended_time": duration} - response = requests.post( - f"{self._versioned_url}/sandboxes/{sandbox_id}/extend", - data=json.dumps(data_dict), - headers=self._auth_headers, - ) - if not response.ok: - raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/extend" + data = {"extended_time": duration} + return self.rest_service.request_post(uri, data).json() def stop_sandbox(self, sandbox_id: str) -> None: """Stop the sandbox given sandbox id""" - self._validate_auth_headers() - response = requests.post( - f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", - headers=self._auth_headers, - ) - if not response.ok: - raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response) + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/stop" + return self.rest_service.request_post(uri).json() # SANDBOX GET REQUESTS def get_sandboxes(self, show_historic=False) -> list: """Get list of sandboxes""" - self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes" + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes" params = {"show_historic": "true" if show_historic else "false"} - response = requests.get(url, headers=self._auth_headers, params=params) - if not response.ok: - err_msg = "Failed to get sandboxes" - raise SandboxRestException(err_msg, response) - return response.json() + return self.rest_service.request_get(uri, params=params).json() def get_sandbox_details(self, sandbox_id: str) -> dict: """Get details of the given sandbox id""" - self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes/{sandbox_id}" - response = requests.get(url, headers=self._auth_headers) - if not response.ok: - exc_msg = f"Failed to get sandbox details for '{sandbox_id}'" - raise SandboxRestException(exc_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}" + return self.rest_service.request_get(uri).json() def get_sandbox_activity( self, @@ -275,8 +232,8 @@ def get_sandbox_activity( 'tail' - how many of the last entries you want to pull 'error_only' - to filter for error events only """ - self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity" + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/activity" params = {} if error_only: @@ -288,103 +245,49 @@ def get_sandbox_activity( if tail: params["tail"] = tail - if params: - response = requests.get(url, headers=self._auth_headers, params=params) - else: - response = requests.get(url, headers=self._auth_headers) - - if not response.ok: - raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response) - return response.json() + return self.rest_service.request_get(uri, params=params).json() def get_sandbox_commands(self, sandbox_id: str) -> list: """Get list of sandbox commands""" - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", - headers=self._auth_headers, - ) - if not response.ok: - raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/commands" + return self.rest_service.request_get(uri).json() def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict: """Get details of specific sandbox command""" - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", - headers=self._auth_headers, - ) - if not response.ok: - err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'" - raise SandboxRestException(err_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/commands/{command_name}" + return self.rest_service.request_get(uri).json() def get_sandbox_components(self, sandbox_id: str) -> list: """Get list of sandbox components""" - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components", - headers=self._auth_headers, - ) - if not response.ok: - err_msg = (response, f"Failed to get components for '{sandbox_id}'") - raise SandboxRestException(err_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components" + return self.rest_service.request_get(uri).json() def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> dict: """Get details of components in sandbox""" - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", - headers=self._auth_headers, - ) - if not response.ok: - custom_err_msg = ( - f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" - ) - raise SandboxRestException(custom_err_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}" + return self.rest_service.request_get(uri).json() def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list: """Get list of commands for a particular component in sandbox""" - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", - headers=self._auth_headers, - ) - if not response.ok: - custom_err_msg = ( - f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" - ) - raise SandboxRestException(custom_err_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}/commands" + return self.rest_service.request_get(uri).json() def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict: """Get details of a command of sandbox component""" - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}", - headers=self._auth_headers, - ) - if not response.ok: - custom_err_msg = ( - f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'" - ) - raise SandboxRestException(custom_err_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}" + return self.rest_service.request_get(uri).json() def get_sandbox_instructions(self, sandbox_id: str) -> str: """ Pull the instructions text of sandbox """ - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", - headers=self._auth_headers, - ) - if not response.ok: - err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'" - raise SandboxRestException(err_msg, response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/instructions" + return self.rest_service.request_get(uri).json() def get_sandbox_output( self, @@ -394,8 +297,8 @@ def get_sandbox_output( since: str = None, ) -> dict: """Get list of sandbox output""" - self._validate_auth_headers() - url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output" + self._validate_auth_header() + uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/output" params = {} if tail: params["tail"] = tail @@ -404,62 +307,39 @@ def get_sandbox_output( if since: params["since"] = since - if params: - response = requests.get(url, headers=self._auth_headers, params=params) - else: - response = requests.get(url, headers=self._auth_headers) - - if not response.ok: - err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'" - raise SandboxRestException(err_msg, response) - return response.json() + return self.rest_service.request_get(uri, params=params).json() # BLUEPRINT GET REQUESTS def get_blueprints(self) -> list: """Get list of blueprints""" - self._validate_auth_headers() - response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers) - if not response.ok: - raise SandboxRestException("Failed to get blueprints", response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/blueprints" + return self.rest_service.request_get(uri).json() def get_blueprint_details(self, blueprint_id: str) -> dict: """ Get details of a specific blueprint Can pass either blueprint name OR blueprint ID """ - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/blueprints/{blueprint_id}", - headers=self._auth_headers, - ) - if not response.ok: - raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/blueprints/{blueprint_id}" + return self.rest_service.request_get(uri).json() # EXECUTIONS def get_execution_details(self, execution_id: str) -> dict: - self._validate_auth_headers() - response = requests.get( - f"{self._versioned_url}/executions/{execution_id}", - headers=self._auth_headers, - ) - if not response.ok: - raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response) - return response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/executions/{execution_id}" + return self.rest_service.request_get(uri).json() def delete_execution(self, execution_id: str) -> None: """ API returns dict with single key on successful deletion of execution {"result": "success"} """ - self._validate_auth_headers() - response = requests.delete( - f"{self._versioned_url}/executions/{execution_id}", - headers=self._auth_headers, - ) - if not response.ok: - raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response) - response_dict = response.json() + self._validate_auth_header() + uri = f"{self._v2_base_uri}/executions/{execution_id}" + response_dict = self.rest_service.request_delete(uri).json() if not response_dict["result"] == "success": - raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}") + raise SandboxRestException( + f"Failed execution deletion of id {execution_id}\n" f"{json.dumps(response_dict, indent=4)}" + ) diff --git a/tests/common.py b/tests/common.py index 6b97c3c..e6d6678 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,4 +1,6 @@ import json +from random import randint +from time import sleep from src.cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @@ -8,6 +10,17 @@ def pretty_print_response(dict_response): print(f"\n{json_str}") +def random_sleep(): + """ To offset the api calls and avoid rate limit quota """ + random = randint(1, 3) + print(f"sleeping {random} seconds") + sleep(random) + + +def fixed_sleep(): + sleep(3) + + def get_blueprint_id_from_name(api: SandboxRestApiSession, bp_name: str): res = api.get_blueprint_details(bp_name) return res["id"] diff --git a/tests/conftest.py b/tests/conftest.py index 99d0c61..0bbf68f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,15 +9,14 @@ @pytest.fixture(scope="session") def admin_session() -> SandboxRestApiSession: - with SandboxRestApiSession( - host=CLOUDSHELL_SERVER, - username=CLOUDSHELL_ADMIN_USER, - password=CLOUDSHELL_ADMIN_PASSWORD, - domain=CLOUDSHELL_DOMAIN, - ) as api: - yield api - time.sleep(2) + admin_api = SandboxRestApiSession( + host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN + ) + with admin_api: + yield admin_api + time.sleep(3) print("admin session token revoked") + print(f"total requests: {admin_api.rest_service.request_counter}") @pytest.fixture(scope="session") diff --git a/tests/test_api_dut_sandbox.py b/tests/test_api_dut_sandbox.py index 7f9d61e..35bf6a9 100644 --- a/tests/test_api_dut_sandbox.py +++ b/tests/test_api_dut_sandbox.py @@ -5,8 +5,6 @@ - DUT model / command can be referenced in constants.py (Putshell / health_check) - Assumed that only one DUT is in blueprint """ -import time - import pytest from common import * from constants import * @@ -27,6 +25,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test") sandbox_id = start_res["id"] print(f"Sandbox started: {sandbox_id}") + fixed_sleep() yield sandbox_id admin_session.stop_sandbox(sandbox_id) print(f"\nSandbox ended: {sandbox_id}") @@ -35,6 +34,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): @pytest.fixture(scope="module") def component_id(admin_session: SandboxRestApiSession, sandbox_id: str): components = admin_session.get_sandbox_components(sandbox_id) + fixed_sleep() component_filter = [x for x in components if x["component_type"] == DUT_MODEL] assert component_filter return component_filter[0]["id"] @@ -44,6 +44,7 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str): def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str): print("Starting DUT Command...") res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND) + fixed_sleep() assert isinstance(res, dict) print("Started execution response") pretty_print_response(res) @@ -54,6 +55,7 @@ def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, componen @pytest.fixture(scope="module") def test_get_execution_details(admin_session, execution_id): res = admin_session.get_execution_details(execution_id) + fixed_sleep() assert isinstance(res, dict) return res @@ -61,7 +63,11 @@ def test_get_execution_details(admin_session, execution_id): def test_delete_execution(admin_session, execution_id, test_get_execution_details): print("Execution Details") pretty_print_response(test_get_execution_details) + is_supports_cancellation = test_get_execution_details["supports_cancellation"] + if not is_supports_cancellation: + print("Can't cancel this command. Returning") + return print("Stopping execution...") - time.sleep(1) admin_session.delete_execution(execution_id) + fixed_sleep() print("Execution deleted") diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py index e326f0b..2681ad5 100644 --- a/tests/test_api_empty_sandbox.py +++ b/tests/test_api_empty_sandbox.py @@ -1,11 +1,9 @@ """ Test the api methods that require an empty, PUBLIC blueprint """ -import time import pytest from common import * -from constants import * from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @@ -28,34 +26,13 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id): print(f"\nSandbox ended: {sandbox_id}") -@pytest.fixture(scope="module") -def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str): - polling_minutes = 2 - counter = 0 - while True: - if counter > polling_minutes: - raise Exception("Timeout waiting for setup to end") - state = admin_session.get_sandbox_details(sandbox_id)["state"] - if state == "Ready": - break - time.sleep(60) - counter += 1 - - print("Rerunning Setup...") - res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND) - assert isinstance(res, dict) - print("Setup re-run execution response") - pretty_print_response(res) - execution_id = res["executionId"] - return execution_id - - def test_start_stop(sandbox_id): assert isinstance(sandbox_id, str) print(f"Sandbox ID: {sandbox_id}") def test_get_sandbox_details(admin_session, sandbox_id): + random_sleep() details_res = admin_session.get_sandbox_details(sandbox_id) assert isinstance(details_res, dict) sb_name = details_res["name"] @@ -63,6 +40,7 @@ def test_get_sandbox_details(admin_session, sandbox_id): def test_get_components(admin_session, sandbox_id): + random_sleep() components_res = admin_session.get_sandbox_components(sandbox_id) assert isinstance(components_res, list) component_count = len(components_res) @@ -70,6 +48,7 @@ def test_get_components(admin_session, sandbox_id): def test_get_sandbox_commands(admin_session, sandbox_id): + random_sleep() commands_res = admin_session.get_sandbox_commands(sandbox_id) assert isinstance(commands_res, list) print(f"Sandbox commands: {[x['name'] for x in commands_res]}") @@ -78,6 +57,7 @@ def test_get_sandbox_commands(admin_session, sandbox_id): def test_get_sandbox_events(admin_session, sandbox_id): + random_sleep() activity_res = admin_session.get_sandbox_activity(sandbox_id) assert isinstance(activity_res, dict) and "events" in activity_res events = activity_res["events"] @@ -85,6 +65,7 @@ def test_get_sandbox_events(admin_session, sandbox_id): def test_get_console_output(admin_session, sandbox_id): + random_sleep() output_res = admin_session.get_sandbox_output(sandbox_id) assert isinstance(output_res, dict) and "entries" in output_res entries = output_res["entries"] @@ -92,12 +73,14 @@ def test_get_console_output(admin_session, sandbox_id): def test_get_instructions(admin_session, sandbox_id): + random_sleep() instructions_res = admin_session.get_sandbox_instructions(sandbox_id) assert isinstance(instructions_res, str) print(f"Pulled sandbox instructions: '{instructions_res}'") def test_extend_sandbox(admin_session, sandbox_id): + random_sleep() extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M") assert isinstance(extend_response, dict) and "remaining_time" in extend_response print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}") diff --git a/tests/test_api_no_sandbox.py b/tests/test_api_no_sandbox.py index f12c19b..93f1f3e 100644 --- a/tests/test_api_no_sandbox.py +++ b/tests/test_api_no_sandbox.py @@ -3,6 +3,7 @@ Live Cloudshell server is still a dependency """ import pytest +from common import * from constants import DEFAULT_EMPTY_BLUEPRINT from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession @@ -20,31 +21,23 @@ def test_delete_token(admin_session: SandboxRestApiSession, api_token: str): admin_session.delete_token(api_token) -def test_reset_session(admin_session: SandboxRestApiSession): - print("\ndeleting admin token...") - admin_session.invalidate_auth() - print("logging back in..") - admin_session.refresh_auth_from_stored_credentials() - print("Calling get blueprints...") - res = admin_session.get_blueprints() - assert isinstance(res, list) - print(f"blueprint count {len(res)}") - - def test_get_sandboxes(admin_session: SandboxRestApiSession): res = admin_session.get_sandboxes() + random_sleep() assert isinstance(res, list) print(f"Sandbox count found in system: {len(res)}") def test_get_blueprints(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprints() + random_sleep() assert isinstance(bp_res, list) print(f"Blueprint count found in system: '{len(bp_res)}'") def test_get_default_blueprint(admin_session: SandboxRestApiSession): bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT) + random_sleep() assert isinstance(bp_res, dict) bp_name = bp_res["name"] print(f"Pulled details for '{bp_name}'") From 81a2895a3ab02d00e22206397a22ae410aaa046f Mon Sep 17 00:00:00 2001 From: katzy687 Date: Tue, 21 Dec 2021 02:12:42 +0200 Subject: [PATCH 29/29] edit signature --- src/cloudshell/sandbox_rest/sandbox_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py index ff732e6..203b9fa 100644 --- a/src/cloudshell/sandbox_rest/sandbox_api.py +++ b/src/cloudshell/sandbox_rest/sandbox_api.py @@ -37,10 +37,10 @@ def __init__( use_https=False, ssl_verify=False, proxies: dict = None, - insecure_warning=False, + show_insecure_warning=False, ): """ Login to api and store headers for future requests """ - super().__init__(host, username, password, token, logger, port, use_https, ssl_verify, proxies, insecure_warning) + super().__init__(host, username, password, token, logger, port, use_https, ssl_verify, proxies, show_insecure_warning) self._base_uri = "/api" self._v2_base_uri = f"{self._base_uri}/v2" self.domain = domain