azure csp unit tests WIP

This commit is contained in:
Philip Kalinsky 2020-02-11 14:11:11 -05:00
parent 50ab045353
commit 441e81ba8d
10 changed files with 355 additions and 149 deletions

View File

@ -54,6 +54,7 @@ selenium = "*"
honcho = "*"
blinker = "*"
pytest-mock = "*"
requests-mock = "*"
detect-secrets = "*"
beautifulsoup4 = "*"
mypy = "*"

151
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "44296f145fcb42cff5fadf14a706ec9598f4436ccbdf05e1d69fcd8316c89e8d"
"sha256": "dcc985866bfecd1e2abcd40722a657f0da223c3c9c11aee32fd4b532eca31cb8"
},
"pipfile-spec": 6,
"requires": {
@ -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": [
@ -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": [
@ -1230,6 +1217,14 @@
"index": "pypi",
"version": "==2.22.0"
},
"requests-mock": {
"hashes": [
"sha256:510df890afe08d36eca5bb16b4aa6308a6f85e3159ad3013bac8b9de7bd5a010",
"sha256:88d3402dd8b3c69a9e4f9d3a73ad11b15920c6efd36bc27bf1f701cf4a8e4646"
],
"index": "pypi",
"version": "==1.7.0"
},
"rope": {
"hashes": [
"sha256:52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203",
@ -1270,10 +1265,10 @@
},
"stevedore": {
"hashes": [
"sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730",
"sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"
"sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b",
"sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"
],
"version": "==1.31.0"
"version": "==1.32.0"
},
"text-unidecode": {
"hashes": [
@ -1361,10 +1356,10 @@
},
"zipp": {
"hashes": [
"sha256:ccc94ed0909b58ffe34430ea5451f07bc0c76467d7081619a454bf5c98b89e28",
"sha256:feae2f18633c32fc71f2de629bfb3bd3c9325cd4419642b1f1da42ee488d9b98"
"sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50",
"sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e"
],
"version": "==2.1.0"
"version": "==2.2.0"
}
}
}

View File

@ -2,6 +2,7 @@ import json
from secrets import token_urlsafe
from typing import Any, Dict
from uuid import uuid4
import pydantic
from atst.utils import sha256_hex
@ -274,31 +275,17 @@ class AzureCloudProvider(CloudProviderInterface):
try:
result = self.sdk.requests.post(
f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview",
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()
result_dict = result.json()
tenant_id = result_dict.get("tenantId")
tenant_admin_username = (
f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com"
)
self.update_tenant_creds(
tenant_id,
KeyVaultCredentials(
tenant_id=tenant_id,
tenant_admin_username=tenant_admin_username,
tenant_admin_password=payload.password,
),
)
return TenantCSPResult(domain_name=payload.domain_name, **result_dict)
except self.sdk.requests.ConnectionError:
app.logger.error(
f"Could not create tenant. Connection Error", exc_info=1,
)
#app.logger.error(
# f"Could not create tenant. Connection Error", exc_info=1,
#)
raise ConnectionException("connection error creating tenant")
except self.sdk.requests.Timeout:
app.logger.error(
@ -308,6 +295,27 @@ class AzureCloudProvider(CloudProviderInterface):
except self.sdk.requests.HTTPError:
raise UnknownServerException("azure application error creating tenant")
result_dict = result.json()
tenant_id = result_dict.get("tenantId")
tenant_admin_username = (
f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com"
)
try:
creds = KeyVaultCredentials(
tenant_id=tenant_id,
tenant_admin_username=tenant_admin_username,
tenant_admin_password=payload.password,
)
except pydantic.ValidationError as val_exc:
return
self.update_tenant_creds(
tenant_id,
creds,
)
return TenantCSPResult(domain_name=payload.domain_name, **result_dict)
def create_billing_profile_creation(
self, payload: BillingProfileCreationCSPPayload
):
@ -330,6 +338,7 @@ class AzureCloudProvider(CloudProviderInterface):
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:
@ -366,7 +375,9 @@ class AzureCloudProvider(CloudProviderInterface):
}
try:
result = self.sdk.requests.get(
payload.billing_profile_verify_url, headers=auth_header
payload.billing_profile_verify_url,
headers=auth_header,
timeout=30,
)
result.raise_for_status()
@ -407,7 +418,12 @@ 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)
result = self.sdk.requests.post(
url,
headers=headers,
json=request_body,
timeout=30,
)
if result.status_code == 201:
return BillingProfileTenantAccessCSPResult(**result.json())
@ -444,7 +460,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
result = self.sdk.requests.patch(
url, headers=request_headers, json=request_body
url, headers=request_headers, json=request_body, timeout=30,
)
result.raise_for_status()
@ -482,7 +498,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
result = self.sdk.requests.get(
payload.task_order_billing_verify_url, headers=auth_header
payload.task_order_billing_verify_url, headers=auth_header, timeout=30,
)
result.raise_for_status()
@ -527,7 +543,7 @@ class AzureCloudProvider(CloudProviderInterface):
}
try:
result = self.sdk.requests.put(url, headers=auth_header, json=request_body)
result = self.sdk.requests.put(url, headers=auth_header, json=request_body, timeout=30)
result.raise_for_status()
return BillingInstructionCSPResult(**result.json())
@ -564,7 +580,7 @@ class AzureCloudProvider(CloudProviderInterface):
}
try:
result = self.sdk.requests.put(url, headers=auth_header, json=request_body)
result = self.sdk.requests.put(url, headers=auth_header, json=request_body, timeout=30)
if result.status_code in [200, 202]:
# 202 has location/retry after headers
return SubscriptionCreationCSPResult(**result.headers, **result.json())
@ -600,7 +616,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
result = self.sdk.requests.get(
payload.subscription_verify_url, headers=auth_header
payload.subscription_verify_url, headers=auth_header, timeout=30
)
result.raise_for_status()
@ -643,6 +659,7 @@ class AzureCloudProvider(CloudProviderInterface):
product_purchase_url,
json=create_product_purchase_body,
headers=create_product_purchase_headers,
timeout=30,
)
result.raise_for_status()
@ -680,7 +697,7 @@ class AzureCloudProvider(CloudProviderInterface):
}
try:
result = self.sdk.requests.get(
payload.product_purchase_verify_url, headers=auth_header
payload.product_purchase_verify_url, headers=auth_header, timeout=30
)
result.raise_for_status()
@ -728,7 +745,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.put(
url, headers=auth_header, json=request_body
url, headers=auth_header, json=request_body, timeout=30
)
response.raise_for_status()
@ -772,7 +789,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.put(
url, headers=auth_header, json=request_body
url, headers=auth_header, json=request_body, timeout=30,
)
response.raise_for_status()
return TenantPrincipalOwnershipCSPResult(**response.json())
@ -809,7 +826,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.post(
url, json=request_body, headers=auth_header
url, json=request_body, headers=auth_header, timeout=30
)
response.raise_for_status()
return TenantPrincipalAppCSPResult(**response.json())
@ -846,7 +863,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.post(
url, json=request_body, headers=auth_header
url, json=request_body, headers=auth_header, timeout=30
)
response.raise_for_status()
return TenantPrincipalCSPResult(**response.json())
@ -887,7 +904,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.post(
url, json=request_body, headers=auth_header
url, json=request_body, headers=auth_header, timeout=30
)
response.raise_for_status()
result = response.json()
@ -932,7 +949,7 @@ class AzureCloudProvider(CloudProviderInterface):
url = f"{self.graph_resource}/beta/roleManagement/directory/roleDefinitions"
try:
response = self.sdk.requests.get(url, headers=auth_header)
response = self.sdk.requests.get(url, headers=auth_header, timeout=30)
response.raise_for_status()
result = response.json()
@ -986,7 +1003,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.post(
url, headers=auth_header, json=request_body
url, headers=auth_header, json=request_body, timeout=30
)
response.raise_for_status()
return PrincipalAdminRoleCSPResult(**response.json())
@ -1104,7 +1121,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.post(
url, headers=auth_header, json=request_body
url, headers=auth_header, json=request_body, timeout=30
)
response.raise_for_status()
@ -1138,7 +1155,7 @@ class AzureCloudProvider(CloudProviderInterface):
try:
response = self.sdk.requests.patch(
url, headers=auth_header, json=request_body
url, headers=auth_header, json=request_body, timeout=30
)
response.raise_for_status()
@ -1257,7 +1274,7 @@ class AzureCloudProvider(CloudProviderInterface):
}
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
try:
result = self.sdk.requests.post(url, headers=auth_header)
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")
@ -1337,6 +1354,7 @@ class AzureCloudProvider(CloudProviderInterface):
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:

View File

@ -122,10 +122,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)

View File

@ -123,15 +123,21 @@ 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__()}'"

View File

@ -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,7 @@ 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 +92,7 @@ class PortfolioStateMachine(
return f"<PortfolioStateMachine(state='{self.current_state.name}', portfolio='{self.portfolio.name}'"
@reconstructor
def attach_machine(self):
def attach_machine(self, stages=AzureStages):
"""
This is called as a result of a sqlalchemy query.
Attach a machine depending on the current state.
@ -86,7 +104,7 @@ class PortfolioStateMachine(
auto_transitions=False,
after_state_change="after_state_change",
)
states, transitions = _build_transitions(AzureStages)
states, transitions = _build_transitions(stages)
self.machine.add_states(self.system_states + states)
self.machine.add_transitions(self.system_transitions + transitions)
@ -120,7 +138,7 @@ class PortfolioStateMachine(
app.logger.info(
f"could not locate 'create trigger' for {self.__repr__()}"
)
self.fail_stage(stage)
self.trigger('fail')
elif self.current_state == FSMStates.FAILED:
# get the first trigger that starts with 'create_'
@ -151,17 +169,16 @@ class PortfolioStateMachine(
if create_trigger is not None:
self.trigger(create_trigger, **kwargs)
def after_in_progress_callback(self, event):
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
def after_in_progress_callback(self, event):
# Accumulate payload w/ creds
payload = event.kwargs.get("csp_data")
payload_data_cls = get_stage_csp_class(stage, "payload")
current_stage = _stage_state_to_stage_name(self.current_state, StageStates.IN_PROGRESS)
payload_data_cls = get_stage_csp_class(current_stage, "payload")
if not payload_data_cls:
app.logger.info(f"could not resolve payload data class for stage {stage}")
self.fail_stage(stage)
app.logger.info(f"could not resolve payload data class for stage {current_stage}")
self.fail_stage(current_stage)
try:
payload_data = payload_data_cls(**payload)
except PydanticValidationError as exc:
@ -171,13 +188,13 @@ class PortfolioStateMachine(
app.logger.info(exc.json())
print(exc.json())
app.logger.info(payload)
self.fail_stage(stage)
self.fail_stage(current_stage)
# TODO: Determine best place to do this, maybe @reconstructor
self.csp = app.csp.cloud
try:
func_name = f"create_{stage}"
func_name = f"create_{current_stage}"
response = getattr(self.csp, func_name)(payload_data)
if self.portfolio.csp_data is None:
self.portfolio.csp_data = {}
@ -193,16 +210,16 @@ class PortfolioStateMachine(
print(exc.json())
app.logger.info(payload_data)
# TODO: Ensure that failing the stage does not preclude a Celery retry
self.fail_stage(stage)
self.fail_stage(current_stage)
# TODO: catch and handle general CSP exception here
except (ConnectionException, UnknownServerException) as exc:
app.logger.error(
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
)
# TODO: Ensure that failing the stage does not preclude a Celery retry
self.fail_stage(stage)
self.fail_stage(current_stage)
self.finish_stage(stage)
self.finish_stage(current_stage)
def is_csp_data_valid(self, event):
"""

View File

@ -1,13 +1,22 @@
import json
from unittest.mock import Mock, patch
from uuid import uuid4
import pendulum
import pydantic
import pytest
#import requests
from secrets import token_urlsafe
from tests.factories import ApplicationFactory, EnvironmentFactory
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
from atst.domain.csp.cloud.exceptions import (
AuthenticationException,
UserProvisioningException,
ConnectionException,
UnknownServerException,
SecretException,
)
from atst.domain.csp.cloud import AzureCloudProvider
from atst.domain.csp.cloud.models import (
AdminRoleDefinitionCSPPayload,
@ -58,6 +67,7 @@ from atst.domain.csp.cloud.models import (
BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = (
spec_dict
@ -93,7 +103,6 @@ def mock_get_secret(azure, val=None):
return azure
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
@ -132,8 +141,8 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
parameters=mock_policy_definition,
)
def test_create_tenant(mock_azure: AzureCloudProvider):
mock_result = Mock()
mock_result.json.return_value = {
"objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
@ -141,7 +150,13 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
"userId": "1153801116406515559",
}
mock_result.status_code = 200
mock_azure.sdk.requests.post.return_value = mock_result
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_result
]
payload = TenantCSPPayload(
**dict(
user_id="admin",
@ -154,10 +169,63 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
)
)
mock_azure = mock_get_secret(mock_azure)
with pytest.raises(ConnectionException):
mock_azure.create_tenant(payload)
result: TenantCSPResult = mock_azure.create_tenant(payload)
assert result.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
def test_create_tenant_req_mock(mock_azure: AzureCloudProvider, requests_mock):
url = f"{mock_azure.sdk.cloud.endpoints.resource_manager}providers/Microsoft.SignUp/createTenant"
requests_mock.register_uri(
"POST", "?".join([url, "api-version=2020-01-01-preview"]),
json={
"objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
"tenantId": "60ff9d34-82bf-4f21-b565-308ef0533435",
"userId": "1153801116406515559",
},
)
mock_azure = mock_get_secret(mock_azure)
payload = TenantCSPPayload(
**dict(
user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2",
first_name="Tedry",
last_name="Tenet",
country_code="US",
password_recovery_email_address="thomas@promptworks.com",
)
)
result: TenantCSPResult = mock_azure.create_tenant(payload)
assert result.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
def test_create_tenant_req_mock_fail(mock_azure: AzureCloudProvider, requests_mock):
url = f"{mock_azure.sdk.cloud.endpoints.resource_manager}providers/Microsoft.SignUp/createTenant"
requests_mock.register_uri(
"POST", "?".join([url, "api-version=2020-01-01-preview"]),
exc=requests.exceptions.ConnectionError,
)
mock_azure = mock_get_secret(mock_azure)
payload = TenantCSPPayload(
**dict(
user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2",
first_name="Tedry",
last_name="Tenet",
country_code="US",
password_recovery_email_address="thomas@promptworks.com",
)
)
with pytest.raises(ConnectionException):
assert mock_azure.create_tenant(payload)
# def test_create_tenant_fails(mock_azure: AzureCloudProvider):
# mock_result = Mock()
# mock_result.status_code = 200

View File

@ -1,7 +1,14 @@
import pytest
from atst.domain.csp import MockCloudProvider
from atst.domain.csp.cloud.models import EnvironmentCSPPayload, EnvironmentCSPResult
from atst.domain.csp.cloud.models import (
EnvironmentCSPPayload,
EnvironmentCSPResult,
TenantCSPPayload,
TenantCSPResult,
BillingProfileCreationCSPPayload,
BillingProfileCreationCSPResult,
)
from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory
@ -28,6 +35,44 @@ def test_create_environment(mock_csp: MockCloudProvider):
assert isinstance(result, EnvironmentCSPResult)
def test_create_tenant(mock_csp: MockCloudProvider):
payload = TenantCSPPayload(
**dict(
user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2",
first_name="Tedry",
last_name="Tenet",
country_code="US",
password_recovery_email_address="thomas@promptworks.com",
)
)
result = mock_csp.create_tenant(payload)
assert isinstance(result, TenantCSPResult)
def test_create_billing_profile_creation(mock_csp: MockCloudProvider):
payload = BillingProfileCreationCSPPayload(
**dict(
address=dict(
address_line_1="123 S Broad Street, Suite 2400",
company_name="Promptworks",
city="Philadelphia",
region="PA",
country="US",
postal_code="19109",
),
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
billing_profile_display_name="Test Billing Profile",
billing_account_name="123123",
)
)
result = mock_csp.create_billing_profile_creation(payload)
assert isinstance(result, BillingProfileCreationCSPResult)
def test_create_or_update_user(mock_csp: MockCloudProvider):
env_role = EnvironmentRoleFactory.create()
csp_user_id = mock_csp.create_or_update_user(CREDENTIALS, env_role, "csp_role_id")

View File

@ -1,6 +1,9 @@
import pytest
import pydantic
import re
from unittest import mock
from enum import Enum
from tests.factories import (
PortfolioStateMachineFactory,
@ -8,13 +11,29 @@ from tests.factories import (
)
from atst.models import FSMStates, PortfolioStateMachine, TaskOrder
from atst.models.mixins.state_machines import AzureStages, StageStates, compose_state
from atst.domain.csp.cloud.models import BillingProfileCreationCSPPayload
from atst.models.mixins.state_machines import (
AzureStages,
StageStates,
compose_state,
_build_transitions,
_build_csp_states,
)
from atst.models.portfolio import Portfolio
from atst.models.portfolio_state_machine import get_stage_csp_class
from atst.models.portfolio_state_machine import (
get_stage_csp_class,
_stage_to_classname,
_stage_state_to_stage_name,
StateMachineMisconfiguredError,
)
# TODO: Write failure case tests
class AzureStagesTest(Enum):
TENANT = "tenant"
@pytest.fixture(scope="function")
def portfolio():
# TODO: setup clin/to as active/funded/ready
@ -37,13 +56,34 @@ def test_state_machine_trigger_next_transition(portfolio):
assert sm.current_state == FSMStates.STARTED
def test_state_machine_compose_state(portfolio):
PortfolioStateMachineFactory.create(portfolio=portfolio)
def test_state_machine_compose_state():
assert (
compose_state(AzureStages.TENANT, StageStates.CREATED)
== FSMStates.TENANT_CREATED
)
def test_stage_to_classname():
assert (_stage_to_classname(AzureStages.BILLING_PROFILE_CREATION.name) == "BillingProfileCreation")
def test_get_stage_csp_class():
csp_class = get_stage_csp_class(list(AzureStages)[0].name.lower(), "payload")
assert isinstance(csp_class, pydantic.main.ModelMetaclass)
def test_get_stage_csp_class_import_fail():
with pytest.raises(StateMachineMisconfiguredError):
csp_class = get_stage_csp_class("doesnotexist", "payload")
def test_build_transitions():
states, transitions = _build_transitions(AzureStagesTest)
assert [s.get('name').name for s in states] == ['TENANT_CREATED', 'TENANT_IN_PROGRESS', 'TENANT_FAILED']
assert [s.get('tags') for s in states] == [['TENANT', 'CREATED'], ['TENANT', 'IN_PROGRESS'], ['TENANT', 'FAILED']]
assert [t.get('trigger') for t in transitions] == ['create_tenant', 'finish_tenant', 'fail_tenant', 'resume_progress_tenant']
def test_build_csp_states():
states = _build_csp_states(AzureStagesTest)
assert list(states) == ['UNSTARTED', 'STARTING', 'STARTED', 'COMPLETED', 'FAILED', 'TENANT_CREATED', 'TENANT_IN_PROGRESS', 'TENANT_FAILED']
def test_state_machine_valid_data_classes_for_stages(portfolio):
PortfolioStateMachineFactory.create(portfolio=portfolio)
@ -52,6 +92,25 @@ def test_state_machine_valid_data_classes_for_stages(portfolio):
assert get_stage_csp_class(stage.name.lower(), "result") is not None
def test_attach_machine(portfolio):
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
sm.machine = None
sm.attach_machine(stages=AzureStagesTest)
assert list(sm.machine.events) == ['init', 'start', 'reset', 'fail', 'create_tenant', 'finish_tenant', 'fail_tenant', 'resume_progress_tenant']
def test_fail_stage(portfolio):
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
sm.state = FSMStates.TENANT_IN_PROGRESS
sm.fail_stage('tenant')
assert sm.state == FSMStates.TENANT_FAILED
#import ipdb;ipdb.set_trace()
def test_stage_state_to_stage_name():
#sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
stage = _stage_state_to_stage_name(FSMStates.TENANT_IN_PROGRESS, StageStates.IN_PROGRESS)
assert stage == "tenant"
def test_state_machine_initialization(portfolio):
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
@ -121,7 +180,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
csp_data = {}
ppoc = portfolio.owner
user_id = f"{ppoc.first_name[0]}{ppoc.last_name}".lower()
user_id = "johndoe" # f"{ppoc.first_name[0]}{ppoc.last_name}".lower()
domain_name = re.sub("[^0-9a-zA-Z]+", "", portfolio.name).lower()
initial_task_order: TaskOrder = portfolio.task_orders[0]
@ -131,10 +190,10 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
"user_id": user_id,
"password": "jklfsdNCVD83nklds2#202", # pragma: allowlist secret
"domain_name": domain_name,
"first_name": ppoc.first_name,
"last_name": ppoc.last_name,
"first_name": "123", # ppoc.first_name,
"last_name": "123", # ppoc.last_name,
"country_code": "US",
"password_recovery_email_address": ppoc.email,
"password_recovery_email_address": "email@example.com", # ppoc.email,
"address": { # TODO: TBD if we're sourcing this from data or config
"company_name": "",
"address_line_1": "",
@ -163,3 +222,4 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
csp_data = portfolio.csp_data
else:
csp_data = {}

View File

@ -79,7 +79,7 @@ def mock_adal():
def mock_requests():
import requests
return Mock(spec=requests)
return Mock(wraps=requests)
def mock_secrets():