Properly report initial clin information

Includes fixed up state machine test as well as adds some missing dependencies
This commit is contained in:
tomdds 2020-01-24 11:01:53 -05:00
parent 81054b2ff0
commit ea040a914e
7 changed files with 208 additions and 81 deletions

View File

@ -36,6 +36,7 @@ transitions = "*"
azure-mgmt-consumption = "*"
adal = "*"
azure-identity = "*"
azure-keyvault = "*"
[dev-packages]
bandit = "*"

82
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "3760f0b1df1156211d671afa2eb417b7bf980aa33d2f74d390e8eed6a3ce8c8b"
"sha256": "4dbb023bcb860eb6dc56e1c201c91f272e1e67ad03e5e5eeb3a7a7fdff350eed"
},
"pipfile-spec": 6,
"requires": {
@ -26,10 +26,10 @@
},
"alembic": {
"hashes": [
"sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d"
"sha256:d412982920653db6e5a44bfd13b1d0db5685cbaaccaf226195749c706e1e862a"
],
"index": "pypi",
"version": "==1.3.2"
"version": "==1.3.3"
},
"amqp": {
"hashes": [
@ -68,6 +68,28 @@
"index": "pypi",
"version": "==1.2.0"
},
"azure-keyvault": {
"hashes": [
"sha256:76f75cb83929f312a08616d426ad6f597f1beae180131cf445876fb88f2c8ef1",
"sha256:e85f5bd6cb4f10b3248b99bbf02e3acc6371d366846897027d4153f18025a2d7"
],
"index": "pypi",
"version": "==4.0.0"
},
"azure-keyvault-keys": {
"hashes": [
"sha256:2983fa42e20a0e6bf6b87976716129c108e613e0292d34c5b0f0c8dc1d488e89",
"sha256:38c27322637a2c52620a8b96da1942ad6a8d22d09b5a01f6fa257f7a51e52ed0"
],
"version": "==4.0.0"
},
"azure-keyvault-secrets": {
"hashes": [
"sha256:2eae9264a8f6f59277e1a9bfdbc8b0a15969ee5a80d8efe403d7744805b4a481",
"sha256:97a602406a833e8f117c540c66059c818f4321a35168dd17365fab1e4527d718"
],
"version": "==4.0.0"
},
"azure-mgmt-authorization": {
"hashes": [
"sha256:31e875a34ac2c5d6fefe77b4a8079a8b2bdbe9edb957e47e8b44222fb212d6a7",
@ -232,6 +254,14 @@
],
"version": "==2.8"
},
"dataclasses": {
"hashes": [
"sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836",
"sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"
],
"markers": "python_version < '3.7'",
"version": "==0.7"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
@ -325,9 +355,9 @@
},
"mako": {
"hashes": [
"sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"
"sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"
],
"version": "==1.1.0"
"version": "==1.1.1"
},
"markupsafe": {
"hashes": [
@ -584,10 +614,10 @@
},
"sqlalchemy": {
"hashes": [
"sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec"
"sha256:64a7b71846db6423807e96820993fa12a03b89127d278290ca25c0b11ed7b4fb"
],
"index": "pypi",
"version": "==1.3.12"
"version": "==1.3.13"
},
"sqlalchemy-json": {
"hashes": [
@ -620,10 +650,10 @@
},
"urllib3": {
"hashes": [
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
],
"version": "==1.25.7"
"version": "==1.25.8"
},
"vine": {
"hashes": [
@ -657,10 +687,10 @@
},
"zipp": {
"hashes": [
"sha256:57147f6b0403b59f33fd357f169f860e031303415aeb7d04ede4839d23905ab8",
"sha256:7ae5ccaca427bafa9760ac3cd8f8c244bfc259794b5b6bb9db4dda2241575d09"
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
],
"version": "==2.0.0"
"version": "==2.0.1"
}
},
"develop": {
@ -671,6 +701,14 @@
],
"version": "==1.4.3"
},
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"argh": {
"hashes": [
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
@ -1062,11 +1100,11 @@
},
"pexpect": {
"hashes": [
"sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
"sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
],
"markers": "sys_platform != 'win32'",
"version": "==4.7.0"
"version": "==4.8.0"
},
"pickleshare": {
"hashes": [
@ -1325,10 +1363,10 @@
},
"urllib3": {
"hashes": [
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
],
"version": "==1.25.7"
"version": "==1.25.8"
},
"watchdog": {
"hashes": [
@ -1359,10 +1397,10 @@
},
"zipp": {
"hashes": [
"sha256:57147f6b0403b59f33fd357f169f860e031303415aeb7d04ede4839d23905ab8",
"sha256:7ae5ccaca427bafa9760ac3cd8f8c244bfc259794b5b6bb9db4dda2241575d09"
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
],
"version": "==2.0.0"
"version": "==2.0.1"
}
}
}

View File

@ -327,12 +327,12 @@ class TaskOrderBillingCreationCSPPayload(BaseCSPPayload):
class TaskOrderBillingCreationCSPResult(AliasModel):
task_order_billing_verify_url: str
retry_after: int
task_order_retry_after: int
class Config:
fields = {
"task_order_billing_verify_url": "Location",
"retry_after": "Retry-After",
"task_order_retry_after": "Retry-After",
}
@ -358,11 +358,11 @@ class TaskOrderBillingVerificationCSPResult(AliasModel):
class BillingInstructionCSPPayload(BaseCSPPayload):
amount: float
start_date: str
end_date: str
clin_type: str
task_order_id: str
initial_clin_amount: float
initial_clin_start_date: str
initial_clin_end_date: str
initial_clin_type: str
initial_task_order_id: str
billing_account_name: str
billing_profile_name: str
@ -646,19 +646,76 @@ class MockCloudProvider(CloudProviderInterface):
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
return {
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/billingRoleAssignments/40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
"name": "40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
return BillingProfileTenantAccessCSPResult(
**{
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/billingRoleAssignments/40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
"name": "40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
"properties": {
"createdOn": "2020-01-14T14:39:26.3342192+00:00",
"createdByPrincipalId": "82e2b376-3297-4096-8743-ed65b3be0b03",
"principalId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
"principalTenantId": "60ff9d34-82bf-4f21-b565-308ef0533435",
"roleDefinitionId": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000000",
"scope": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB",
},
"type": "Microsoft.Billing/billingRoleAssignments",
}
).dict()
def create_task_order_billing_creation(self, payload: TaskOrderBillingCreationCSPPayload):
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)
return TaskOrderBillingCreationCSPResult(**{"Location": "https://somelocation", "Retry-After": "10"}).dict()
def create_task_order_billing_verification(self, payload: TaskOrderBillingVerificationCSPPayload):
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)
return TaskOrderBillingVerificationCSPResult(**{
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/XC36-GRNZ-BG7-TGB",
"name": "XC36-GRNZ-BG7-TGB",
"properties": {
"createdOn": "2020-01-14T14:39:26.3342192+00:00",
"createdByPrincipalId": "82e2b376-3297-4096-8743-ed65b3be0b03",
"principalId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
"principalTenantId": "60ff9d34-82bf-4f21-b565-308ef0533435",
"roleDefinitionId": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000000",
"scope": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB",
"address": {
"addressLine1": "123 S Broad Street, Suite 2400",
"city": "Philadelphia",
"companyName": "Promptworks",
"country": "US",
"postalCode": "19109",
"region": "PA"
},
"currency": "USD",
"displayName": "First Portfolio Billing Profile",
"enabledAzurePlans": [
{
"productId": "DZH318Z0BPS6",
"skuId": "0001",
"skuDescription": "Microsoft Azure Plan"
}
],
"hasReadAccess": True,
"invoiceDay": 5,
"invoiceEmailOptIn": False
},
"type": "Microsoft.Billing/billingRoleAssignments",
}
"type": "Microsoft.Billing/billingAccounts/billingProfiles"
}).dict()
def create_billing_instruction(self, payload: BillingInstructionCSPPayload):
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)
return BillingInstructionCSPResult(**{
"name": "TO1:CLIN001",
"properties": {
"amount": 1000.0,
"endDate": "2020-03-01T00:00:00+00:00",
"startDate": "2020-01-01T00:00:00+00:00"
},
"type": "Microsoft.Billing/billingAccounts/billingProfiles/billingInstructions"
}).dict()
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
self._authorize(auth_credentials)
@ -1150,13 +1207,13 @@ class AzureCloudProvider(CloudProviderInterface):
request_body = {
"properties": {
"amount": payload.amount,
"startDate": payload.start_date,
"endDate": payload.end_date,
"amount": payload.initial_clin_amount,
"startDate": payload.initial_clin_start_date,
"endDate": payload.initial_clin_end_date,
}
}
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.task_order_id}:CLIN00{payload.clin_type}?api-version=2019-10-01-preview"
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview"
auth_header = {
"Authorization": f"Bearer {sp_token}",

View File

@ -67,7 +67,6 @@ class PortfolioStateMachine(
def __repr__(self):
return f"<PortfolioStateMachine(state='{self.current_state.name}', portfolio='{self.portfolio.name}'"
@reconstructor
def attach_machine(self):
"""
@ -112,7 +111,9 @@ class PortfolioStateMachine(
if create_trigger:
self.trigger(create_trigger, **kwargs)
else:
app.logger.info(f"could not locate 'create trigger' for {self.__repr__()}")
app.logger.info(
f"could not locate 'create trigger' for {self.__repr__()}"
)
self.fail_stage(stage)
elif state_obj.is_CREATED:
@ -143,8 +144,11 @@ class PortfolioStateMachine(
try:
payload_data = payload_data_cls(**payload)
except PydanticValidationError as exc:
app.logger.error(f"Payload Validation Error in {self.__repr__()}:", exc_info=1)
app.logger.error(
f"Payload Validation Error in {self.__repr__()}:", exc_info=1
)
app.logger.info(exc.json())
print(exc.json())
app.logger.info(payload)
self.fail_stage(stage)
@ -161,7 +165,10 @@ class PortfolioStateMachine(
func_name = f"create_{stage}"
response = getattr(self.csp, func_name)(payload_data)
except (ConnectionException, UnknownServerException) as exc:
app.logger.error(f"CSP api call. Caught exception for {self.__repr__()}. Retry attempt {attempt}", exc_info=1)
app.logger.error(
f"CSP api call. Caught exception for {self.__repr__()}. Retry attempt {attempt}",
exc_info=1,
)
continue
else:
break
@ -198,12 +205,16 @@ class PortfolioStateMachine(
dc = cls(**stage_data)
if getattr(dc, "get_creds", None) is not None:
new_creds = dc.get_creds()
print("creds to report")
print(new_creds)
# TODO: how/where to store these
# TODO: credential schema
# self.store_creds(self.portfolio, new_creds)
except PydanticValidationError as exc:
app.logger.error(f"Payload Validation Error in {self.__repr__()}:", exc_info=1)
app.logger.error(
f"Payload Validation Error in {self.__repr__()}:", exc_info=1
)
app.logger.info(exc.json())
app.logger.info(payload)

View File

@ -404,11 +404,11 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
payload = BillingInstructionCSPPayload(
**dict(
creds=creds,
amount=1000.00,
start_date="2020/1/1",
end_date="2020/3/1",
clin_type="1",
task_order_id="TO1",
initial_clin_amount=1000.00,
initial_clin_start_date="2020/1/1",
initial_clin_end_date="2020/3/1",
initial_clin_type="1",
initial_task_order_id="TO1",
billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
billing_profile_name="KQWI-W2SU-BG7-TGB",
)

View File

@ -4,16 +4,19 @@ import re
from tests.factories import (
PortfolioFactory,
PortfolioStateMachineFactory,
TaskOrderFactory,
CLINFactory,
)
from atst.models import FSMStates, PortfolioStateMachine
from atst.models import FSMStates, PortfolioStateMachine, TaskOrder
from atst.models.mixins.state_machines import AzureStages, StageStates, compose_state
from atst.models.portfolio import Portfolio
from atst.domain.csp import get_stage_csp_class
@pytest.fixture(scope="function")
def portfolio():
portfolio = PortfolioFactory.create()
portfolio = CLINFactory.create().task_order.portfolio
return portfolio
@ -77,7 +80,7 @@ def test_state_machine_initialization(portfolio):
assert ["reset", "fail", create_trigger] == started_triggers
def test_fsm_transition_start(portfolio):
def test_fsm_transition_start(portfolio: Portfolio):
sm: PortfolioStateMachine = PortfolioStateMachineFactory.create(portfolio=portfolio)
assert sm.portfolio
assert sm.state == FSMStates.UNSTARTED
@ -88,6 +91,16 @@ def test_fsm_transition_start(portfolio):
sm.start()
assert sm.state == FSMStates.STARTED
expected_states = [
FSMStates.TENANT_CREATED,
FSMStates.BILLING_PROFILE_CREATION_CREATED,
FSMStates.BILLING_PROFILE_VERIFICATION_CREATED,
FSMStates.BILLING_PROFILE_TENANT_ACCESS_CREATED,
FSMStates.TASK_ORDER_BILLING_CREATION_CREATED,
FSMStates.TASK_ORDER_BILLING_VERIFICATION_CREATED,
FSMStates.BILLING_INSTRUCTION_CREATED,
]
# Should source all creds for portfolio? might be easier to manage than per-step specific ones
creds = {"username": "mock-cloud", "password": "shh"}
if portfolio.csp_data is not None:
@ -95,19 +108,21 @@ def test_fsm_transition_start(portfolio):
else:
csp_data = {}
# ppoc = portfolio.owner
# user_id = f"{ppoc.first_name[0]}{ppoc.last_name}".lower()
user_id = "abcdefg"
ppoc = portfolio.owner
user_id = 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]
initial_clin = initial_task_order.sorted_clins[0]
portfolio_data = {
"user_id": user_id,
"password": "jklfsdNCVD83nklds2#202",
"domain_name": domain_name,
"first_name": "john", # ppoc.first_name,
"last_name": "doe", # ppoc.last_name,
"first_name": ppoc.first_name,
"last_name": ppoc.last_name,
"country_code": "US",
"password_recovery_email_address": "email@example.com", # ppoc.email,
"password_recovery_email_address": ppoc.email,
"address": {
"company_name": "",
"address_line_1": "",
@ -117,26 +132,24 @@ def test_fsm_transition_start(portfolio):
"postal_code": "",
},
"billing_profile_display_name": "My Billing Profile",
"initial_clin_amount": initial_clin.obligated_amount,
"initial_clin_start_date": initial_clin.start_date.strftime("%Y/%m/%d"),
"initial_clin_end_date": initial_clin.end_date.strftime("%Y/%m/%d"),
"initial_clin_type": initial_clin.number,
"initial_task_order_id": initial_task_order.number,
}
config = {"billing_account_name": "billing_account_name"}
collected_data = dict(
list(csp_data.items()) + list(portfolio_data.items()) + list(config.items())
)
sm.trigger_next_transition(creds=creds, csp_data=collected_data)
for expected_state in expected_states:
print(expected_state)
collected_data = dict(
list(csp_data.items()) + list(portfolio_data.items()) + list(config.items())
)
sm.trigger_next_transition(creds=creds, csp_data=collected_data)
assert sm.state == expected_state
if portfolio.csp_data is not None:
csp_data = portfolio.csp_data
else:
csp_data = {}
assert sm.state == FSMStates.TENANT_CREATED
assert portfolio.csp_data.get("tenant_id", None) is not None
#print(portfolio.csp_data.keys())
if portfolio.csp_data is not None:
csp_data = portfolio.csp_data
else:
csp_data = {}
collected_data = dict(
list(csp_data.items()) + list(portfolio_data.items()) + list(config.items())
)
sm.trigger_next_transition(creds=creds, csp_data=collected_data)
assert sm.state == FSMStates.BILLING_PROFILE_CREATION_CREATED
#print(portfolio.csp_data.keys())

View File

@ -66,6 +66,12 @@ def mock_requests():
return Mock(spec=requests)
def mock_secrets():
from azure.keyvault import secrets
return Mock(spec=secrets)
class MockAzureSDK(object):
def __init__(self):
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
@ -78,6 +84,7 @@ class MockAzureSDK(object):
self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials()
self.policy = mock_policy()
self.secrets = mock_secrets()
self.requests = mock_requests()
# may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD