From 34c616ce638784f70e59afd11d7812e5f1adad65 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 17 Sep 2019 10:30:09 -0400 Subject: [PATCH 1/4] First pass at Cloud Provision Exceptions --- atst/domain/csp/cloud.py | 169 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 4a6961e5..ac4c6165 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -11,6 +11,138 @@ class GeneralCSPException(Exception): pass +class OperationInProgressException(GeneralCSPException): + """Throw this for instances when the CSP reports that the current entity is already + being operated on/created/deleted/etc + """ + + def __init__(self, operation_desc): + self.operation_desc = operation_desc + + @property + def message(self): + return "An operation for this entity is already in progress: {}".format( + self.operation_desc + ) + + +class AuthorizationException(GeneralCSPException): + """Throw this for instances when there is a problem with the auth credentials: + * Missing credentials + * Incorrect credentials + * Credentials not authorized for current action? (should this be it's own error?) + """ + + def __init__(self, auth_error): + self.auth_error = auth_error + + @property + def message(self): + return "An error occurred with authorization: {}".format(self.auth_error) + + +class ConnectionException(GeneralCSPException): + """A general problem with the connection, timeouts or unresolved endpoints + """ + + def __init__(self, connection_error): + self.connection_error = connection_error + + @property + def message(self): + return "Could not connect to cloud provider: {}".format(self.connection_error) + + +class UnknownServerException(GeneralCSPException): + """An error occured on the CSP side (5xx) and we don't know why + """ + + def __init__(self, server_error): + self.server_error = server_error + + @property + def message(self): + return "A server error occured: {}".format(self.server_error) + + +class EnvironmentExistsException(GeneralCSPException): + """If the environment you're attempting to provision either already exists + or is in the process of being created, throw this exception + """ + + def __init__(self, env_identifier): + self.env_identifier = env_identifier + + @property + def message(self): + return "The environment {} already exists or is already being created".format( + self.env_identifier + ) + + +class EnvironmentCreationException(GeneralCSPException): + """If there was an error in creating the environment + """ + + def __init__(self, env_identifier, reason): + self.env_identifier = env_identifier + self.reason = reason + + @property + def message(self): + return "The envionment {} couldn't be created: {}".format( + self.env_identifier, self.reason + ) + + +class UserProvisioningError(GeneralCSPException): + """Failed to provision a user + """ + + def __init__(self, env_identifier, user_identifier, reason): + self.env_identifier = env_identifier + self.user_identifier = user_identifier + self.reason = reason + + @property + def message(self): + return "Failed to create user {} for environment {}: {}".format( + self.user_identifier, self.env_identifier, self.reason + ) + + +class UserRemovalException(GeneralCSPException): + """Failed to remove a user + """ + + def __init__(self, env_identifier, user_identifier, reason): + self.env_identifier = env_identifier + self.user_identifier = user_identifier + self.reason = reason + + @property + def message(self): + return "Failed to remove user {} for environment {}: {}".format( + self.user_identifier, self.env_identifier, self.reason + ) + + +class BaselineProvisionException(GeneralCSPException): + """If there's any issues standing up whatever is required + for an environment baseline + """ + + def __init__(self, env_identifier, reason): + self.env_identifier = env_identifier + self.reason = reason + + @property + def message(self): + return "Could not complete baseline provisioning for environment ({}): {}".format( + self.env_identifier, self.reason + ) + + class CloudProviderInterface: def root_creds() -> Dict: raise NotImplementedError() @@ -27,6 +159,13 @@ class CloudProviderInterface: Returns: string: ID of created environment + + Raises: + AuthorizationException: Problem with the credentials + ConnectionException: Issue with the CSP API connection + UnknownServerException: Unknown issue on the CSP side + EnvironmentExistsException: Environment already exists and has been created + OperationInProgressException: Envrionment creation already in progress """ raise NotImplementedError() @@ -47,6 +186,12 @@ class CloudProviderInterface: "user_id": string, "credentials": dict, # structure TBD based on csp } + + Raises: + AuthorizationException: Problem with the credentials + ConnectionException: Issue with the CSP API connection + UnknownServerException: Unknown issue on the CSP side + UserProvisioningError: Problem creating the root user """ raise NotImplementedError() @@ -61,6 +206,11 @@ class CloudProviderInterface: Returns: dict: Returns dict that associates the resource identities with their ATAT representations. + Raises: + AuthorizationException: Problem with the credentials + ConnectionException: Issue with the CSP API connection + UnknownServerException: Unknown issue on the CSP side + BaselineProvisionException: Specific issue occurred with some aspect of baseline setup """ raise NotImplementedError() @@ -77,6 +227,13 @@ class CloudProviderInterface: Returns: string: Returns the interal csp_user_id of the created/updated user account + + Raises: + AuthorizationException: Problem with the credentials + ConnectionException: Issue with the CSP API connection + UnknownServerException: Unknown issue on the CSP side + UserProvisioningError: User couldn't be created + UserModificationException: User couldn't be modified """ raise NotImplementedError() @@ -90,6 +247,12 @@ class CloudProviderInterface: Returns: bool -- True on success + + Raises: + AuthorizationException: Problem with the credentials + ConnectionException: Issue with the CSP API connection + UnknownServerException: Unknown issue on the CSP side + UserModificationException: User couldn't be modified """ raise NotImplementedError() @@ -104,7 +267,11 @@ class CloudProviderInterface: bool -- True on success Raises: - TBDException: Some part of user deletion failed + AuthorizationException: Problem with the credentials + ConnectionException: Issue with the CSP API connection + UnknownServerException: Unknown issue on the CSP side + UserModificationException: User couldn't be modified + UserRemovalException: User couldn't be removed """ raise NotImplementedError() From 08f98a557fac1aae45f6f375673c97cb51943df3 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 17 Sep 2019 15:34:50 -0400 Subject: [PATCH 2/4] Create separate authentication and authorization exceptions --- atst/domain/csp/cloud.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index ac4c6165..d8d28cb0 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -26,11 +26,24 @@ class OperationInProgressException(GeneralCSPException): ) -class AuthorizationException(GeneralCSPException): +class AuthenticationException(GeneralCSPException): """Throw this for instances when there is a problem with the auth credentials: * Missing credentials * Incorrect credentials - * Credentials not authorized for current action? (should this be it's own error?) + * Other credential problems + """ + + def __init__(self, auth_error): + self.auth_error = auth_error + + @property + def message(self): + return "An error occurred with authentication: {}".format(self.auth_error) + + +class AuthorizationException(GeneralCSPException): + """Throw this for instances when the current credentials are not authorized + for the current action. """ def __init__(self, auth_error): @@ -161,7 +174,8 @@ class CloudProviderInterface: string: ID of created environment Raises: - AuthorizationException: Problem with the credentials + AuthenticationException: Problem with the credentials + AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side EnvironmentExistsException: Environment already exists and has been created @@ -188,7 +202,8 @@ class CloudProviderInterface: } Raises: - AuthorizationException: Problem with the credentials + AuthenticationException: Problem with the credentials + AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side UserProvisioningError: Problem creating the root user @@ -207,7 +222,8 @@ class CloudProviderInterface: Returns: dict: Returns dict that associates the resource identities with their ATAT representations. Raises: - AuthorizationException: Problem with the credentials + AuthenticationException: Problem with the credentials + AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side BaselineProvisionException: Specific issue occurred with some aspect of baseline setup @@ -229,7 +245,8 @@ class CloudProviderInterface: string: Returns the interal csp_user_id of the created/updated user account Raises: - AuthorizationException: Problem with the credentials + AuthenticationException: Problem with the credentials + AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side UserProvisioningError: User couldn't be created @@ -249,7 +266,8 @@ class CloudProviderInterface: bool -- True on success Raises: - AuthorizationException: Problem with the credentials + AuthenticationException: Problem with the credentials + AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side UserModificationException: User couldn't be modified @@ -267,7 +285,8 @@ class CloudProviderInterface: bool -- True on success Raises: - AuthorizationException: Problem with the credentials + AuthenticationException: Problem with the credentials + AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side UserModificationException: User couldn't be modified From 5aa5acfb2aaec2a5e93a301ecd3c9759163f1448 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 17 Sep 2019 16:51:05 -0400 Subject: [PATCH 3/4] Remove redundant EnvironmentExistsException --- atst/domain/csp/cloud.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index d8d28cb0..faf4707a 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -78,21 +78,6 @@ class UnknownServerException(GeneralCSPException): return "A server error occured: {}".format(self.server_error) -class EnvironmentExistsException(GeneralCSPException): - """If the environment you're attempting to provision either already exists - or is in the process of being created, throw this exception - """ - - def __init__(self, env_identifier): - self.env_identifier = env_identifier - - @property - def message(self): - return "The environment {} already exists or is already being created".format( - self.env_identifier - ) - - class EnvironmentCreationException(GeneralCSPException): """If there was an error in creating the environment """ From ff8119acd1066964853a29544cc5d44087342057 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 23 Sep 2019 15:09:59 -0400 Subject: [PATCH 4/4] Integrate errors with MockCloudProvider --- atst/domain/csp/cloud.py | 74 ++++++++++++++++++++++++++--------- tests/domain/test_mock_csp.py | 9 ++++- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index faf4707a..bbcbc830 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -93,7 +93,7 @@ class EnvironmentCreationException(GeneralCSPException): ) -class UserProvisioningError(GeneralCSPException): +class UserProvisioningException(GeneralCSPException): """Failed to provision a user """ @@ -113,15 +113,14 @@ class UserRemovalException(GeneralCSPException): """Failed to remove a user """ - def __init__(self, env_identifier, user_identifier, reason): - self.env_identifier = env_identifier - self.user_identifier = user_identifier + def __init__(self, user_csp_id, reason): + self.user_csp_id = user_csp_id self.reason = reason @property def message(self): - return "Failed to remove user {} for environment {}: {}".format( - self.user_identifier, self.env_identifier, self.reason + return "Failed to suspend or delete user {}: {}".format( + self.user_csp_id, self.reason ) @@ -164,7 +163,6 @@ class CloudProviderInterface: ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side EnvironmentExistsException: Environment already exists and has been created - OperationInProgressException: Envrionment creation already in progress """ raise NotImplementedError() @@ -191,7 +189,7 @@ class CloudProviderInterface: AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side - UserProvisioningError: Problem creating the root user + UserProvisioningException: Problem creating the root user """ raise NotImplementedError() @@ -234,8 +232,7 @@ class CloudProviderInterface: AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side - UserProvisioningError: User couldn't be created - UserModificationException: User couldn't be modified + UserProvisioningException: User couldn't be created or modified """ raise NotImplementedError() @@ -255,7 +252,7 @@ class CloudProviderInterface: AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side - UserModificationException: User couldn't be modified + UserRemovalException: User couldn't be suspended """ raise NotImplementedError() @@ -274,7 +271,6 @@ class CloudProviderInterface: AuthorizationException: Credentials not authorized for current action(s) ConnectionException: Issue with the CSP API connection UnknownServerException: Unknown issue on the CSP side - UserModificationException: User couldn't be modified UserRemovalException: User couldn't be removed """ raise NotImplementedError() @@ -295,12 +291,16 @@ class CloudProviderInterface: class MockCloudProvider(CloudProviderInterface): # TODO: All of these constants - AUTH_EXCEPTION = GeneralCSPException("Authentication failure.") - NETWORK_EXCEPTION = GeneralCSPException("Network failure.") + AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.") + AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.") + NETWORK_EXCEPTION = ConnectionException("Network failure.") + SERVER_EXCEPTION = UnknownServerException("Not our fault.") + SERVER_FAILURE_PCT = 1 NETWORK_FAILURE_PCT = 7 ENV_CREATE_FAILURE_PCT = 12 ATAT_ADMIN_CREATE_FAILURE_PCT = 12 + UNAUTHORIZED_RATE = 2 def __init__(self, config, with_delay=True, with_failure=True): from time import sleep @@ -319,10 +319,14 @@ class MockCloudProvider(CloudProviderInterface): self._delay(1, 5) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) self._maybe_raise( self.ENV_CREATE_FAILURE_PCT, - GeneralCSPException("Could not create environment."), + EnvironmentCreationException( + environment.id, "Could not create environment." + ), ) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) return self._id() @@ -331,11 +335,16 @@ class MockCloudProvider(CloudProviderInterface): self._delay(1, 5) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) self._maybe_raise( self.ATAT_ADMIN_CREATE_FAILURE_PCT, - GeneralCSPException("Could not create admin user."), + UserProvisioningException( + csp_environment_id, "atat_admin", "Could not create admin user." + ), ) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + return {"id": self._id(), "credentials": self._auth_credentials} def create_environment_baseline(self, auth_credentials, csp_environment_id): @@ -343,11 +352,15 @@ class MockCloudProvider(CloudProviderInterface): self._delay(1, 5) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) self._maybe_raise( self.ATAT_ADMIN_CREATE_FAILURE_PCT, - GeneralCSPException("Could not create environment baseline."), + BaselineProvisionException( + csp_environment_id, "Could not create environment baseline." + ), ) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) return { CSPRole.BASIC_ACCESS.value: self._id(), CSPRole.NETWORK_ADMIN.value: self._id(), @@ -360,19 +373,42 @@ class MockCloudProvider(CloudProviderInterface): self._delay(1, 5) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) self._maybe_raise( self.ATAT_ADMIN_CREATE_FAILURE_PCT, - GeneralCSPException("Could not create user."), + UserProvisioningException( + user_info.environment.id, + user_info.application_role.user_id, + "Could not create user.", + ), ) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) return self._id() def suspend_user(self, auth_credentials, csp_user_id): + self._authorize(auth_credentials) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + + self._maybe_raise( + self.ATAT_ADMIN_CREATE_FAILURE_PCT, + UserRemovalException(csp_user_id, "Could not suspend user."), + ) + return self._maybe(12) def delete_user(self, auth_credentials, csp_user_id): + self._authorize(auth_credentials) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + + self._maybe_raise( + self.ATAT_ADMIN_CREATE_FAILURE_PCT, + UserRemovalException(csp_user_id, "Could not delete user."), + ) + + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) return self._maybe(12) def get_calculator_url(self): @@ -405,4 +441,4 @@ class MockCloudProvider(CloudProviderInterface): def _authorize(self, credentials): self._delay(1, 5) if credentials != self._auth_credentials: - raise self.AUTH_EXCEPTION + raise self.AUTHENTICATION_EXCEPTION diff --git a/tests/domain/test_mock_csp.py b/tests/domain/test_mock_csp.py index 941479b7..93b8dfc8 100644 --- a/tests/domain/test_mock_csp.py +++ b/tests/domain/test_mock_csp.py @@ -2,6 +2,8 @@ import pytest from atst.domain.csp import MockCloudProvider +from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory + CREDENTIALS = MockCloudProvider(config={})._auth_credentials @@ -11,7 +13,9 @@ def mock_csp(): def test_create_environment(mock_csp: MockCloudProvider): - environment_id = mock_csp.create_environment(CREDENTIALS, {}, {}) + environment = EnvironmentFactory.create() + user = UserFactory.create() + environment_id = mock_csp.create_environment(CREDENTIALS, user, environment) assert isinstance(environment_id, str) @@ -27,7 +31,8 @@ def test_create_environment_baseline(mock_csp: MockCloudProvider): def test_create_or_update_user(mock_csp: MockCloudProvider): - csp_user_id = mock_csp.create_or_update_user(CREDENTIALS, {}, "csp_role_id") + env_role = EnvironmentRoleFactory.create() + csp_user_id = mock_csp.create_or_update_user(CREDENTIALS, env_role, "csp_role_id") assert isinstance(csp_user_id, str)