diff --git a/Pipfile.lock b/Pipfile.lock index 391f766d..f34446d9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -26,10 +26,10 @@ }, "alembic": { "hashes": [ - "sha256:d412982920653db6e5a44bfd13b1d0db5685cbaaccaf226195749c706e1e862a" + "sha256:2df2519a5b002f881517693b95626905a39c5faf4b5a1f94de4f1441095d1d26" ], "index": "pypi", - "version": "==1.3.3" + "version": "==1.4.0" }, "amqp": { "hashes": [ @@ -47,10 +47,10 @@ }, "azure-core": { "hashes": [ - "sha256:b8ccbd901d085048e4e3e72627b066923c5bd3780e4c43cf9cf9948aee9bdf9e", - "sha256:e2cd99f0c0aef12c168d498cb5bc47a3a45c8ab08112183e3ec97e4dcb33ceb9" + "sha256:8bdb12b8e937c5bdf495faadf7741ea1958436e2d24c9c5ca7bd7a5ca7a9e42f", + "sha256:bcfc4502c4cfbdcbe82301119439f52542fa4a42dfc1dadd647bba8b01819823" ], - "version": "==1.2.1" + "version": "==1.2.2" }, "azure-graphrbac": { "hashes": [ @@ -62,11 +62,11 @@ }, "azure-identity": { "hashes": [ - "sha256:4ce65058461c277991763ed3f121efc6b9eb9c2edefb62c414dfa85c814690d3", - "sha256:b32acd1cdb6202bfe10d9a0858dc463d8960295da70ae18097eb3b85ab12cb91" + "sha256:17fa904e0447fd2a2dc19909379edb769b05656dbaf4863b8c4fdfb2bb54350c", + "sha256:7e9c85e3f82f1e29e5edfc7beb3030b25e8b8fd02b65d5ea1c67f13cde01da0f" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.3.0" }, "azure-keyvault": { "hashes": [ @@ -78,17 +78,17 @@ }, "azure-keyvault-keys": { "hashes": [ - "sha256:2983fa42e20a0e6bf6b87976716129c108e613e0292d34c5b0f0c8dc1d488e89", - "sha256:38c27322637a2c52620a8b96da1942ad6a8d22d09b5a01f6fa257f7a51e52ed0" + "sha256:1c230c052b9f0b9ecaee97347fe4ebf3fcc798f92edbfd618ea264efc61ad554", + "sha256:711af402a0000ac329406253470c1198cc452adc8638608461ae54c8dce92afc" ], - "version": "==4.0.0" + "version": "==4.0.1" }, "azure-keyvault-secrets": { "hashes": [ - "sha256:2eae9264a8f6f59277e1a9bfdbc8b0a15969ee5a80d8efe403d7744805b4a481", - "sha256:97a602406a833e8f117c540c66059c818f4321a35168dd17365fab1e4527d718" + "sha256:0afd85eaa94962fc8ad9e71348ba0873d723d531163894ee3175f138c3180fe4", + "sha256:0f92705444f55ca0d5a892172eda898f42678602834f03fa8012e9979e5fe619" ], - "version": "==4.0.0" + "version": "==4.0.1" }, "azure-mgmt-authorization": { "hashes": [ @@ -116,11 +116,11 @@ }, "azure-mgmt-resource": { "hashes": [ - "sha256:455a10bbae15673c7879d7515b38e1548cb1a8982dd35029ab3192565262c573", - "sha256:c2ad10cab63999c0a88ee498bc36200ee7f6e6e5d4bf82712bde882eda11146f" + "sha256:a77707bad5551bd558da450045cd2f7097fb8cbaf68610a510a9e413f8a9cf3e", + "sha256:d90b7d8f237b71b54cfd06480dc1ecd7dac81b22301bf2f4ead98a53cf269b6a" ], "index": "pypi", - "version": "==8.0.0" + "version": "==8.0.1" }, "azure-mgmt-subscription": { "hashes": [ @@ -186,41 +186,36 @@ }, "cffi": { "hashes": [ - "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", - "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", - "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", - "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", - "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", - "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", - "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", - "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", - "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", - "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", - "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", - "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", - "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", - "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", - "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", - "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", - "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", - "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", - "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", - "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", - "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", - "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", - "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", - "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", - "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", - "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", - "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", - "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", - "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", - "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", - "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", - "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", - "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" + "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", + "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", + "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", + "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", + "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", + "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", + "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", + "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", + "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", + "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", + "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", + "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", + "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", + "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", + "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", + "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", + "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", + "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", + "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", + "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", + "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", + "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", + "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", + "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", + "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", + "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", + "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", + "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" ], - "version": "==1.13.2" + "version": "==1.14.0" }, "chardet": { "hashes": [ @@ -296,11 +291,11 @@ }, "flask-wtf": { "hashes": [ - "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", - "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" + "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", + "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720" ], "index": "pypi", - "version": "==0.14.2" + "version": "==0.14.3" }, "idna": { "hashes": [ @@ -333,10 +328,10 @@ }, "jinja2": { "hashes": [ - "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", - "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" + "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", + "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" ], - "version": "==2.11.1" + "version": "==3.0.0a1" }, "kombu": { "hashes": [ @@ -685,10 +680,10 @@ }, "zipp": { "hashes": [ - "sha256:ccc94ed0909b58ffe34430ea5451f07bc0c76467d7081619a454bf5c98b89e28", - "sha256:feae2f18633c32fc71f2de629bfb3bd3c9325cd4419642b1f1da42ee488d9b98" + "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50", + "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e" ], - "version": "==2.1.0" + "version": "==2.2.0" } }, "develop": { @@ -699,14 +694,6 @@ ], "version": "==1.4.3" }, - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, "astroid": { "hashes": [ "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", @@ -871,10 +858,10 @@ }, "gitpython": { "hashes": [ - "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", - "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" + "sha256:99c77677f31f255e130f3fed4c8e0eebb35f1a09df98ff965fff6774f71688cf", + "sha256:99cd0403cecd8a13b95d2e045b9fcaa7837137fcc5ec3105f2c413305d82c143" ], - "version": "==3.0.5" + "version": "==3.0.7" }, "honcho": { "hashes": [ @@ -944,10 +931,10 @@ }, "jinja2": { "hashes": [ - "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", - "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" + "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", + "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" ], - "version": "==2.11.1" + "version": "==3.0.0a1" }, "lazy-object-proxy": { "hashes": [ @@ -1056,10 +1043,10 @@ }, "parso": { "hashes": [ - "sha256:1376bdc8cb81377ca481976933773295218a2df47d3e1182ba76d372b1acb128", - "sha256:597f36de5102a8db05ffdf7ecdc761838b86565a4a111604c6e78beaedf1b045" + "sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57", + "sha256:951af01f61e6dccd04159042a0706a31ad437864ec6e25d0d7a96a9fbb9b0095" ], - "version": "==0.6.0" + "version": "==0.6.1" }, "pathspec": { "hashes": [ @@ -1158,11 +1145,11 @@ }, "pytest-flask": { "hashes": [ - "sha256:283730b469604ecb94caac28df99a40b7c785b828dd8d3323596718b51dfaeb2", - "sha256:d874781b622210d8c5d8061cdb091cb059fcb12203125110bd8e6f9256ccbf49" + "sha256:9001f6128c5c4a0d243ce46c117f3691052828d2faf39ac151b8388657dce447", + "sha256:cbd8c5b9f8f1b83e9c159ac4294964807c4934317a5fba181739ac15e1b823e6" ], "index": "pypi", - "version": "==0.15.0" + "version": "==0.15.1" }, "pytest-mock": { "hashes": [ @@ -1270,10 +1257,10 @@ }, "stevedore": { "hashes": [ - "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", - "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" + "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b", + "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b" ], - "version": "==1.31.0" + "version": "==1.32.0" }, "text-unidecode": { "hashes": [ @@ -1361,10 +1348,10 @@ }, "zipp": { "hashes": [ - "sha256:ccc94ed0909b58ffe34430ea5451f07bc0c76467d7081619a454bf5c98b89e28", - "sha256:feae2f18633c32fc71f2de629bfb3bd3c9325cd4419642b1f1da42ee488d9b98" + "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50", + "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e" ], - "version": "==2.1.0" + "version": "==2.2.0" } } } diff --git a/atst/domain/csp/cloud/azure_cloud_provider.py b/atst/domain/csp/cloud/azure_cloud_provider.py index b85bed09..679d5cbd 100644 --- a/atst/domain/csp/cloud/azure_cloud_provider.py +++ b/atst/domain/csp/cloud/azure_cloud_provider.py @@ -1,14 +1,17 @@ import json from secrets import token_urlsafe from uuid import uuid4 +from flask import current_app as app from atst.utils import sha256_hex from .cloud_provider_interface import CloudProviderInterface from .exceptions import ( AuthenticationException, - SecretException, UserProvisioningException, + ConnectionException, + UnknownServerException, + SecretException, ) from .models import ( AdminRoleDefinitionCSPPayload, @@ -96,7 +99,7 @@ class AzureSDKProvider(object): self.graphrbac = graphrbac self.credentials = credentials self.identity = identity - self.exceptions = exceptions + self.azure_exceptions = exceptions self.secrets = secrets self.requests = requests self.cloud = AZURE_PUBLIC_CLOUD @@ -133,14 +136,15 @@ class AzureCloudProvider(CloudProviderInterface): ) try: return secret_client.set_secret(secret_key, secret_value) - except self.sdk.exceptions.HttpResponseError as exc: + except self.sdk.azure_exceptions.HttpResponseError: app.logger.error( f"Could not SET secret in Azure keyvault for key {secret_key}.", exc_info=1, ) + creds = self._source_creds() raise SecretException( + creds.tenant_id, f"Could not SET secret in Azure keyvault for key {secret_key}.", - exc.message, ) def get_secret(self, secret_key): @@ -150,14 +154,15 @@ class AzureCloudProvider(CloudProviderInterface): ) try: return secret_client.get_secret(secret_key).value - except self.sdk.exceptions.HttpResponseError: + except self.sdk.azure_exceptions.HttpResponseError: app.logger.error( f"Could not GET secret in Azure keyvault for key {secret_key}.", exc_info=1, ) + creds = self._source_creds() raise SecretException( + creds.tenant_id, f"Could not GET secret in Azure keyvault for key {secret_key}.", - exc.message, ) def create_environment(self, payload: EnvironmentCSPPayload): @@ -318,29 +323,49 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - result = self.sdk.requests.post( - f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview", - json=create_tenant_body, - headers=create_tenant_headers, + try: + result = self.sdk.requests.post( + f"{self.sdk.cloud.endpoints.resource_manager}providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview", + json=create_tenant_body, + headers=create_tenant_headers, + timeout=30, + ) + result.raise_for_status() + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create tenant. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating tenant") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create tenant. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error creating tenant") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating tenant", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating tenant. {str(exc)}", + ) + + result_dict = result.json() + tenant_id = result_dict.get("tenantId") + tenant_admin_username = f"{payload.user_id}@{payload.domain_name}.{self.config.get('OFFICE_365_DOMAIN')}" + self.update_tenant_creds( + tenant_id, + KeyVaultCredentials( + tenant_id=tenant_id, + tenant_admin_username=tenant_admin_username, + tenant_admin_password=payload.password, + ), ) - if result.status_code == 200: - result_dict = result.json() - tenant_id = result_dict.get("tenantId") - tenant_admin_username = f"{payload.user_id}@{payload.domain_name}.{self.config.get('OFFICE_365_DOMAIN')}" - self.update_tenant_creds( - tenant_id, - KeyVaultCredentials( - tenant_id=tenant_id, - tenant_admin_username=tenant_admin_username, - tenant_admin_password=payload.password, - ), - ) - return self._ok( - TenantCSPResult(domain_name=payload.domain_name, **result_dict) - ) - else: - return self._error(result.json()) + return TenantCSPResult(domain_name=payload.domain_name, **result_dict) def create_billing_profile_creation( self, payload: BillingProfileCreationCSPPayload @@ -359,20 +384,41 @@ class AzureCloudProvider(CloudProviderInterface): billing_account_create_url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview" - result = self.sdk.requests.post( - billing_account_create_url, - json=create_billing_account_body, - headers=create_billing_account_headers, - ) + try: + result = self.sdk.requests.post( + billing_account_create_url, + json=create_billing_account_body, + headers=create_billing_account_headers, + timeout=30, + ) + result.raise_for_status() + if result.status_code == 202: + # 202 has location/retry after headers + return BillingProfileCreationCSPResult(**result.headers) + elif result.status_code == 200: + # NB: Swagger docs imply call can sometimes resolve immediately + return BillingProfileVerificationCSPResult(**result.json()) - if result.status_code == 202: - # 202 has location/retry after headers - return self._ok(BillingProfileCreationCSPResult(**result.headers)) - elif result.status_code == 200: - # NB: Swagger docs imply call can sometimes resolve immediately - return self._ok(BillingProfileVerificationCSPResult(**result.json())) - else: - return self._error(result.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create billing profile. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating billing profile") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create billing profile. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error creating tenant") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating billing profile", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating billing profile. {str(exc)}", + ) def create_billing_profile_verification( self, payload: BillingProfileVerificationCSPPayload @@ -386,18 +432,44 @@ class AzureCloudProvider(CloudProviderInterface): auth_header = { "Authorization": f"Bearer {sp_token}", } + try: + result = self.sdk.requests.get( + payload.billing_profile_verify_url, headers=auth_header, timeout=30, + ) + result.raise_for_status() - result = self.sdk.requests.get( - payload.billing_profile_verify_url, headers=auth_header - ) + if result.status_code == 202: + # 202 has location/retry after headers + return BillingProfileCreationCSPResult(**result.headers) + elif result.status_code == 200: + return BillingProfileVerificationCSPResult(**result.json()) - if result.status_code == 202: - # 202 has location/retry after headers - return self._ok(BillingProfileCreationCSPResult(**result.headers)) - elif result.status_code == 200: - return self._ok(BillingProfileVerificationCSPResult(**result.json())) - else: - return self._error(result.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not verify billing profile creation. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error creating billing profile verification" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not verify billing profile creation. Request timed out.", + exc_info=1, + ) + raise ConnectionException( + "timout error during billing profile verification" + ) + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error during billing profile verification", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error during billing profile verification. {str(exc)}", + ) def create_billing_profile_tenant_access( self, payload: BillingProfileTenantAccessCSPPayload @@ -416,12 +488,40 @@ class AzureCloudProvider(CloudProviderInterface): } url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview" + try: + result = self.sdk.requests.post( + url, headers=headers, json=request_body, timeout=30, + ) + result.raise_for_status() + if result.status_code == 201: + return BillingProfileTenantAccessCSPResult(**result.json()) - result = self.sdk.requests.post(url, headers=headers, json=request_body) - if result.status_code == 201: - return self._ok(BillingProfileTenantAccessCSPResult(**result.json())) - else: - return self._error(result.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create billing profile tenant access. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error creating billing profile tenant access" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create billing profile tenant access. Request timed out.", + exc_info=1, + ) + raise ConnectionException( + "timout error creating billing profile tenant access" + ) + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating billing profile tenant access", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating billing profile tenant access. {str(exc)}", + ) def create_task_order_billing_creation( self, payload: TaskOrderBillingCreationCSPPayload @@ -441,17 +541,38 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview" - result = self.sdk.requests.patch( - url, headers=request_headers, json=request_body - ) + try: + result = self.sdk.requests.patch( + url, headers=request_headers, json=request_body, timeout=30, + ) + result.raise_for_status() - if result.status_code == 202: - # 202 has location/retry after headers - return self._ok(TaskOrderBillingCreationCSPResult(**result.headers)) - elif result.status_code == 200: - return self._ok(TaskOrderBillingVerificationCSPResult(**result.json())) - else: - return self._error(result.json()) + if result.status_code == 202: + # 202 has location/retry after headers + return TaskOrderBillingCreationCSPResult(**result.headers) + elif result.status_code == 200: + return TaskOrderBillingVerificationCSPResult(**result.json()) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create task order billing. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating task order billing") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create task order billing. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error creating task order billing") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating task order billing", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating task order billing. {str(exc)}", + ) def create_task_order_billing_verification( self, payload: TaskOrderBillingVerificationCSPPayload @@ -466,17 +587,41 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - result = self.sdk.requests.get( - payload.task_order_billing_verify_url, headers=auth_header - ) + try: + result = self.sdk.requests.get( + payload.task_order_billing_verify_url, headers=auth_header, timeout=30, + ) + result.raise_for_status() - if result.status_code == 202: - # 202 has location/retry after headers - return self._ok(TaskOrderBillingCreationCSPResult(**result.headers)) - elif result.status_code == 200: - return self._ok(TaskOrderBillingVerificationCSPResult(**result.json())) - else: - return self._error(result.json()) + if result.status_code == 202: + # 202 has location/retry after headers + return TaskOrderBillingCreationCSPResult(**result.headers) + elif result.status_code == 200: + return TaskOrderBillingVerificationCSPResult(**result.json()) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not verify task order billing. Connection Error", exc_info=1, + ) + raise ConnectionException( + "connection error during task order billing verification" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create verify task order billing. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error in task order billing verification") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error in task order billing verification", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error in task order billing verification. {str(exc)}", + ) def create_billing_instruction(self, payload: BillingInstructionCSPPayload): sp_token = self._get_root_provisioning_token() @@ -499,12 +644,34 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - result = self.sdk.requests.put(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.put( + url, headers=auth_header, json=request_body, timeout=30 + ) + result.raise_for_status() + return BillingInstructionCSPResult(**result.json()) - if result.status_code == 200: - return self._ok(BillingInstructionCSPResult(**result.json())) - else: - return self._error(result.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create billing instructions. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating billing instructions") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create billing instructions. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error creating billing instructions") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error in creating billing instructions", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error in creating billing instructions. {str(exc)}", + ) def create_subscription(self, payload: SubscriptionCreationCSPPayload): sp_token = self._get_tenant_principal_token(payload.tenant_id) @@ -525,13 +692,35 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - result = self.sdk.requests.put(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.put( + url, headers=auth_header, json=request_body, timeout=30 + ) + result.raise_for_status() + if result.status_code in [200, 202]: + # 202 has location/retry after headers + return SubscriptionCreationCSPResult(**result.headers, **result.json()) - if result.status_code in [200, 202]: - # 202 has location/retry after headers - return SubscriptionCreationCSPResult(**result.headers, **result.json()) - else: - return self._error(result.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create subscription. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating subscription") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create subscription. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error creating subscription") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating subscription", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating subscription. {str(exc)}", + ) def create_subscription_creation(self, payload: SubscriptionCreationCSPPayload): return self.create_subscription(payload) @@ -549,15 +738,37 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - result = self.sdk.requests.get( - payload.subscription_verify_url, headers=auth_header - ) + try: + result = self.sdk.requests.get( + payload.subscription_verify_url, headers=auth_header, timeout=30 + ) + result.raise_for_status() - if result.ok: # 202 has location/retry after headers return SuscriptionVerificationCSPResult(**result.json()) - else: - return self._error(result.json()) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not verify subscription. Connection Error", exc_info=1, + ) + raise ConnectionException( + "connection error during subscription verification" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not verify subscription. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error during subscription verification") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error during subscription verification", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error during subscription verification. {str(exc)}", + ) def create_product_purchase(self, payload: ProductPurchaseCSPPayload): sp_token = self._get_root_provisioning_token() @@ -577,21 +788,42 @@ class AzureCloudProvider(CloudProviderInterface): } product_purchase_url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/purchaseProduct?api-version=2019-10-01-preview" + try: + result = self.sdk.requests.post( + product_purchase_url, + json=create_product_purchase_body, + headers=create_product_purchase_headers, + timeout=30, + ) + result.raise_for_status() - result = self.sdk.requests.post( - product_purchase_url, - json=create_product_purchase_body, - headers=create_product_purchase_headers, - ) + if result.status_code == 202: + # 202 has location/retry after headers + return ProductPurchaseCSPResult(**result.headers) + elif result.status_code == 200: + # NB: Swagger docs imply call can sometimes resolve immediately + return ProductPurchaseVerificationCSPResult(**result.json()) - if result.status_code == 202: - # 202 has location/retry after headers - return self._ok(ProductPurchaseCSPResult(**result.headers)) - elif result.status_code == 200: - # NB: Swagger docs imply call can sometimes resolve immediately - return self._ok(ProductPurchaseVerificationCSPResult(**result.json())) - else: - return self._error(result.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not purchase product. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error during product purchase") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not purchase product. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error during product purchase") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error during product purchase", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error during product purchase. {str(exc)}", + ) def create_product_purchase_verification( self, payload: ProductPurchaseVerificationCSPPayload @@ -605,23 +837,45 @@ class AzureCloudProvider(CloudProviderInterface): auth_header = { "Authorization": f"Bearer {sp_token}", } + try: + result = self.sdk.requests.get( + payload.product_purchase_verify_url, headers=auth_header, timeout=30 + ) + result.raise_for_status() - result = self.sdk.requests.get( - payload.product_purchase_verify_url, headers=auth_header - ) - - if result.status_code == 202: - # 202 has location/retry after headers - return self._ok(ProductPurchaseCSPResult(**result.headers)) - elif result.status_code == 200: - premium_purchase_date = result.json()["properties"]["purchaseDate"] - return self._ok( - ProductPurchaseVerificationCSPResult( + if result.status_code == 202: + # 202 has location/retry after headers + return ProductPurchaseCSPResult(**result.headers) + elif result.status_code == 200: + premium_purchase_date = result.json()["properties"]["purchaseDate"] + return ProductPurchaseVerificationCSPResult( premium_purchase_date=premium_purchase_date ) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not verify product purchase. Connection Error", exc_info=1, + ) + raise ConnectionException( + "connection error during product purchase verification" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not verify product purchase. Request timed out.", exc_info=1, + ) + raise ConnectionException( + "timout error during product purchase verification" + ) + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error during product purchase verification", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error during product purchase verification. {str(exc)}", ) - else: - return self._error(result.json()) def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload): mgmt_token = self._get_elevated_management_token(payload.tenant_id) @@ -643,10 +897,38 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01" - response = self.sdk.requests.put(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.put( + url, headers=auth_header, json=request_body, timeout=30 + ) + result.raise_for_status() - if response.ok: - return TenantAdminOwnershipCSPResult(**response.json()) + return TenantAdminOwnershipCSPResult(**result.json()) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create tenant admin ownership. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error creating tenant admin ownership" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create tenant admin ownership. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error creating tenant admin ownership") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating tenant admin ownership", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating tenant admin ownership. {str(exc)}", + ) def create_tenant_principal_ownership( self, payload: TenantPrincipalOwnershipCSPPayload @@ -671,10 +953,39 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01" - response = self.sdk.requests.put(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.put( + url, headers=auth_header, json=request_body, timeout=30, + ) + result.raise_for_status() + return TenantPrincipalOwnershipCSPResult(**result.json()) - if response.ok: - return TenantPrincipalOwnershipCSPResult(**response.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create tenant principal ownership. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error creating tenant principal ownership" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create tenant principal ownership. Request timed out.", + exc_info=1, + ) + raise ConnectionException( + "timout error creating tenant prinicpal ownership" + ) + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating tenant principal ownership", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating tenant principal ownership. {str(exc)}", + ) def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload): graph_token = self._get_tenant_admin_token( @@ -693,10 +1004,34 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}/v1.0/applications" - response = self.sdk.requests.post(url, json=request_body, headers=auth_header) + try: + result = self.sdk.requests.post( + url, json=request_body, headers=auth_header, timeout=30 + ) + result.raise_for_status() + return TenantPrincipalAppCSPResult(**result.json()) - if response.ok: - return TenantPrincipalAppCSPResult(**response.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create tenant principal app. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating tenant principal app") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create tenant principal app. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error creating tenant principal app") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating tenant principal app", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating tenant principal app. {str(exc)}", + ) def create_tenant_principal(self, payload: TenantPrincipalCSPPayload): graph_token = self._get_tenant_admin_token( @@ -715,10 +1050,33 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}/beta/servicePrincipals" - response = self.sdk.requests.post(url, json=request_body, headers=auth_header) + try: + result = self.sdk.requests.post( + url, json=request_body, headers=auth_header, timeout=30 + ) + result.raise_for_status() + return TenantPrincipalCSPResult(**result.json()) - if response.ok: - return TenantPrincipalCSPResult(**response.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create tenant principal. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating tenant principal") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create tenant principal. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error creating tenant principal") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating tenant principal", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating tenant principal. {str(exc)}", + ) def create_tenant_principal_credential( self, payload: TenantPrincipalCredentialCSPPayload @@ -741,15 +1099,17 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}/v1.0/applications/{payload.principal_app_object_id}/addPassword" - response = self.sdk.requests.post(url, json=request_body, headers=auth_header) - - if response.ok: - result = response.json() + try: + result = self.sdk.requests.post( + url, json=request_body, headers=auth_header, timeout=30 + ) + result.raise_for_status() + result_json = result.json() self.update_tenant_creds( payload.tenant_id, KeyVaultCredentials( tenant_id=payload.tenant_id, - tenant_sp_key=result.get("secretText"), + tenant_sp_key=result_json.get("secretText"), tenant_sp_client_id=payload.principal_app_id, ), ) @@ -758,6 +1118,33 @@ class AzureCloudProvider(CloudProviderInterface): principal_creds_established=True, ) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create tenant principal credential. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error creating tenant principal credential" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create tenant principal credential. Request timed out.", + exc_info=1, + ) + raise ConnectionException( + "timout error creating tenant principal credential" + ) + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating tenant principal credential", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating tenant principal credential. {str(exc)}", + ) + def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload): graph_token = self._get_tenant_admin_token( payload.tenant_id, self.graph_resource @@ -772,23 +1159,46 @@ class AzureCloudProvider(CloudProviderInterface): } url = f"{self.graph_resource}/beta/roleManagement/directory/roleDefinitions" + try: + response = self.sdk.requests.get(url, headers=auth_header, timeout=30) + response.raise_for_status() - response = self.sdk.requests.get(url, headers=auth_header) + result = response.json() + roleList = result.get("value") - result = response.json() - roleList = result.get("value") + DEFAULT_ADMIN_RD_ID = "794bb258-3e31-42ff-9ee4-731a72f62851" + admin_role_def_id = next( + ( + role.get("id") + for role in roleList + if role.get("displayName") == "Company Administrator" + ), + DEFAULT_ADMIN_RD_ID, + ) - DEFAULT_ADMIN_RD_ID = "794bb258-3e31-42ff-9ee4-731a72f62851" - admin_role_def_id = next( - ( - role.get("id") - for role in roleList - if role.get("displayName") == "Company Administrator" - ), - DEFAULT_ADMIN_RD_ID, - ) + return AdminRoleDefinitionCSPResult(admin_role_def_id=admin_role_def_id) - return AdminRoleDefinitionCSPResult(admin_role_def_id=admin_role_def_id) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create admin role definition. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating admin role definition") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create admin role definition. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error creating admin role definition") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + response.status_code, + "azure application error creating admin role definition", + exc_info=1, + ) + raise UnknownServerException( + response.status_code, + f"azure application error creating admin role definition. {str(exc)}", + ) def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload): graph_token = self._get_tenant_admin_token( @@ -811,10 +1221,34 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}/beta/roleManagement/directory/roleAssignments" - response = self.sdk.requests.post(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.post( + url, headers=auth_header, json=request_body, timeout=30 + ) + result.raise_for_status() + return PrincipalAdminRoleCSPResult(**result.json()) - if response.ok: - return PrincipalAdminRoleCSPResult(**response.json()) + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create principal admin role. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating principal admin role") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create principal admin role. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error creating principal admin role") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating principal admin role", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating principal admin role. {str(exc)}", + ) def create_billing_owner(self, payload: BillingOwnerCSPPayload): graph_token = self._get_tenant_principal_token( @@ -851,9 +1285,31 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}/beta/roleManagement/directory/roleAssignments" - response = self.sdk.requests.post(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.post(url, headers=auth_header, json=request_body) + result.raise_for_status() + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not assign billing owner role. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error assigning billing owner role") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not assign billing owner role. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error assigning billing owner role") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error assigning billing owner role", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error assigning billing owner role. {str(exc)}", + ) - if response.ok: + if result.ok: return True else: raise UserProvisioningException("Could not assign billing admin role") @@ -864,11 +1320,32 @@ class AzureCloudProvider(CloudProviderInterface): } url = f"{self.graph_resource}/v1.0/directoryRoles" + try: + result = self.sdk.requests.get(url, headers=auth_header) + result.raise_for_status() + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not get billing owner role. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error getting billing owner role") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not get billing owner role. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error getting billing owner role") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error getting billing owner role", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error getting billing owner role. {str(exc)}", + ) - response = self.sdk.requests.get(url, headers=auth_header) - - if response.ok: - result = response.json() + if result.ok: + result = result.json() for role in result["value"]: if role["displayName"] == "Billing Administrator": return role["id"] @@ -877,12 +1354,6 @@ class AzureCloudProvider(CloudProviderInterface): "Could not find Billing Administrator role ID; role may not be enabled." ) - def force_tenant_admin_pw_update(self, creds, tenant_owner_id): - # use creds to update to force password recovery? - # not sure what the endpoint/method for this is, yet - - return self._ok() - def _get_management_service_principal(self): # we really should be using graph.microsoft.com, but i'm getting # "expired token" errors for that @@ -975,12 +1446,35 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}v1.0/users" - response = self.sdk.requests.post(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.post( + url, headers=auth_header, json=request_body, timeout=30 + ) + result.raise_for_status() - if response.ok: - return UserCSPResult(**response.json()) - else: - raise UserProvisioningException(f"Failed to create user: {response.json()}") + return UserCSPResult(**result.json()) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not create active directory user. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error creating active directory user") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not create active directory user. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error creating active directory user") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error creating active directory user", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error creating active directory user. {str(exc)}", + ) def _update_active_directory_user_email(self, graph_token, user_id, payload): request_body = {"otherMails": [payload.email]} @@ -991,13 +1485,44 @@ class AzureCloudProvider(CloudProviderInterface): url = f"{self.graph_resource}v1.0/users/{user_id}" - response = self.sdk.requests.patch(url, headers=auth_header, json=request_body) + try: + result = self.sdk.requests.patch( + url, headers=auth_header, json=request_body, timeout=30 + ) + result.raise_for_status() - if response.ok: - return True - else: - raise UserProvisioningException( - f"Failed update user email: {response.json()}" + if result.ok: + return True + else: + raise UserProvisioningException( + f"Failed update user email: {response.json()}" + ) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not update active directory user email. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error updating active directory user email" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not update active directory user email. Request timed out.", + exc_info=1, + ) + raise ConnectionException( + "timout error updating active directory user email" + ) + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error updating active directory user email", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error updating active directory user email. {str(exc)}", ) def create_user_role(self, payload: UserRoleCSPPayload): @@ -1098,24 +1623,6 @@ class AzureCloudProvider(CloudProviderInterface): client_secret=creds.root_sp_key, ) - def _ok(self, body=None): - return self._make_response("ok", body) - - def _error(self, body=None): - return self._make_response("error", body) - - def _make_response(self, status, body=dict()): - """Create body for responses from API - - Arguments: - status {string} -- "ok" or "error" - body {dict} -- dict containing details of response or error, if applicable - - Returns: - dict -- status of call with body containing details - """ - return {"status": status, "body": body} - @property def _root_creds(self): return { @@ -1146,12 +1653,38 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {mgmt_token}", } url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01" - result = self.sdk.requests.post(url, headers=auth_header) + try: + result = self.sdk.requests.post(url, headers=auth_header, timeout=30) + result.raise_for_status() + if not result.ok: + raise AuthenticationException("Failed to elevate access") - if not result.ok: - raise AuthenticationException("Failed to elevate access") + return mgmt_token - return mgmt_token + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not get elevated management token. Connection Error", + exc_info=1, + ) + raise ConnectionException( + "connection error getting elevated management token" + ) + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not get elevated management token. Request timed out.", + exc_info=1, + ) + raise ConnectionException("timout error getting elevated management token") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error getting elevated management token", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error getting elevated management token. {str(exc)}", + ) def _source_creds(self, tenant_id=None) -> KeyVaultCredentials: if tenant_id: @@ -1205,10 +1738,35 @@ class AzureCloudProvider(CloudProviderInterface): cost_mgmt_url = ( f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01" ) - result = self.sdk.requests.post( - f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}", - json=request_body, - headers=headers, - ) - if result.ok: - return CostManagementQueryCSPResult(**result.json()) + + try: + result = self.sdk.requests.post( + f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}", + json=request_body, + headers=headers, + timeout=30, + ) + result.raise_for_status() + if result.ok: + return CostManagementQueryCSPResult(**result.json()) + + except self.sdk.requests.exceptions.ConnectionError: + app.logger.error( + f"Could not get reporting data. Connection Error", exc_info=1, + ) + raise ConnectionException("connection error getting reporting data") + except self.sdk.requests.exceptions.Timeout: + app.logger.error( + f"Could not get reporting data. Request timed out.", exc_info=1, + ) + raise ConnectionException("timout error getting reporting data") + except self.sdk.requests.exceptions.HTTPError as exc: + app.logger.error( + result.status_code, + "azure application error getting reporting data", + exc_info=1, + ) + raise UnknownServerException( + result.status_code, + f"azure application error getting reporting data. {str(exc)}", + ) diff --git a/atst/domain/csp/cloud/exceptions.py b/atst/domain/csp/cloud/exceptions.py index 3480180f..57eddd5d 100644 --- a/atst/domain/csp/cloud/exceptions.py +++ b/atst/domain/csp/cloud/exceptions.py @@ -61,12 +61,13 @@ class UnknownServerException(GeneralCSPException): """An error occured on the CSP side (5xx) and we don't know why """ - def __init__(self, server_error): + def __init__(self, status_code, server_error): + self.status_code = status_code self.server_error = server_error @property def message(self): - return "A server error occured: {}".format(self.server_error) + return f"A server error with status code [{self.status_code}] occured: {self.server_error}" class EnvironmentCreationException(GeneralCSPException): diff --git a/atst/domain/csp/cloud/mock_cloud_provider.py b/atst/domain/csp/cloud/mock_cloud_provider.py index 7371357a..a34f40d5 100644 --- a/atst/domain/csp/cloud/mock_cloud_provider.py +++ b/atst/domain/csp/cloud/mock_cloud_provider.py @@ -72,7 +72,7 @@ class MockCloudProvider(CloudProviderInterface): AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.") AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.") NETWORK_EXCEPTION = ConnectionException("Network failure.") - SERVER_EXCEPTION = UnknownServerException("Not our fault.") + SERVER_EXCEPTION = UnknownServerException(500, "Not our fault.") SERVER_FAILURE_PCT = 1 NETWORK_FAILURE_PCT = 7 @@ -129,10 +129,6 @@ class MockCloudProvider(CloudProviderInterface): payload is an instance of TenantCSPPayload data class """ - self._authorize("admin") - - 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.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) diff --git a/atst/models/mixins/state_machines.py b/atst/models/mixins/state_machines.py index 41af66ba..8c941ffe 100644 --- a/atst/models/mixins/state_machines.py +++ b/atst/models/mixins/state_machines.py @@ -109,6 +109,14 @@ def _build_transitions(csp_stages): dest=compose_state(csp_stage, StageStates.FAILED), ) ) + transitions.append( + dict( + trigger="resume_progress_" + csp_stage.name.lower(), + source=compose_state(csp_stage, StageStates.FAILED), + dest=compose_state(csp_stage, StageStates.IN_PROGRESS), + conditions=["is_ready_resume_progress"], + ) + ) return states, transitions @@ -130,15 +138,20 @@ class FSMMixin: ] def fail_stage(self, stage): - fail_trigger = "fail" + stage + fail_trigger = f"fail_{stage}" + if fail_trigger in self.machine.get_triggers(self.current_state.name): self.trigger(fail_trigger) app.logger.info( f"calling fail trigger '{fail_trigger}' for '{self.__repr__()}'" ) + else: + app.logger.info( + f"could not locate fail trigger '{fail_trigger}' for '{self.__repr__()}'" + ) def finish_stage(self, stage): - finish_trigger = "finish_" + stage + finish_trigger = f"finish_{stage}" if finish_trigger in self.machine.get_triggers(self.current_state.name): app.logger.info( f"calling finish trigger '{finish_trigger}' for '{self.__repr__()}'" diff --git a/atst/models/portfolio_state_machine.py b/atst/models/portfolio_state_machine.py index 595fb8fe..848d2b03 100644 --- a/atst/models/portfolio_state_machine.py +++ b/atst/models/portfolio_state_machine.py @@ -15,13 +15,31 @@ from atst.database import db from atst.models.types import Id from atst.models.base import Base import atst.models.mixins as mixins -from atst.models.mixins.state_machines import FSMStates, AzureStages, _build_transitions +from atst.models.mixins.state_machines import ( + FSMStates, + AzureStages, + StageStates, + _build_transitions, +) + + +class StateMachineMisconfiguredError(Exception): + def __init__(self, class_details): + self.class_details = class_details + + @property + def message(self): + return self.class_details def _stage_to_classname(stage): return "".join(map(lambda word: word.capitalize(), stage.split("_"))) +def _stage_state_to_stage_name(state, stage_state): + return state.name.split(f"_{stage_state.name}")[0].lower() + + def get_stage_csp_class(stage, class_type): """ given a stage name and class_type return the class @@ -34,7 +52,9 @@ def get_stage_csp_class(stage, class_type): importlib.import_module("atst.domain.csp.cloud.models"), cls_name ) except AttributeError: - print("could not import CSP Result class <%s>" % cls_name) + raise StateMachineMisconfiguredError( + f"could not import CSP Payload/Result class {cls_name}" + ) @add_state_features(Tags) @@ -74,7 +94,7 @@ class PortfolioStateMachine( return f"