Merge pull request #1317 from dod-ccpo/azure-custom-integration
First Pass Azure Tenant Creation Integration + Orchestration
This commit is contained in:
commit
02ec54a310
4
Pipfile
4
Pipfile
@ -33,6 +33,10 @@ azure-mgmt-authorization = "*"
|
||||
azure-mgmt-managementgroups = "*"
|
||||
azure-mgmt-resource = "*"
|
||||
transitions = "*"
|
||||
azure-mgmt-consumption = "*"
|
||||
adal = "*"
|
||||
azure-identity = "*"
|
||||
azure-keyvault = "*"
|
||||
|
||||
[dev-packages]
|
||||
bandit = "*"
|
||||
|
134
Pipfile.lock
generated
134
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "63b8f9d203f306a6f0ff20514b024909aa7e64917e1befcc9ea79931b5b4bd34"
|
||||
"sha256": "4dbb023bcb860eb6dc56e1c201c91f272e1e67ad03e5e5eeb3a7a7fdff350eed"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -21,14 +21,15 @@
|
||||
"sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf",
|
||||
"sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.2"
|
||||
},
|
||||
"alembic": {
|
||||
"hashes": [
|
||||
"sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d"
|
||||
"sha256:d412982920653db6e5a44bfd13b1d0db5685cbaaccaf226195749c706e1e862a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.2"
|
||||
"version": "==1.3.3"
|
||||
},
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
@ -44,6 +45,13 @@
|
||||
],
|
||||
"version": "==1.1.24"
|
||||
},
|
||||
"azure-core": {
|
||||
"hashes": [
|
||||
"sha256:b8ccbd901d085048e4e3e72627b066923c5bd3780e4c43cf9cf9948aee9bdf9e",
|
||||
"sha256:e2cd99f0c0aef12c168d498cb5bc47a3a45c8ab08112183e3ec97e4dcb33ceb9"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"azure-graphrbac": {
|
||||
"hashes": [
|
||||
"sha256:53e98ae2ca7c19b349e9e9bb1b6a824aeae8dcfcbe17190d20fe69c0f185b2e2",
|
||||
@ -52,6 +60,36 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.61.1"
|
||||
},
|
||||
"azure-identity": {
|
||||
"hashes": [
|
||||
"sha256:4ce65058461c277991763ed3f121efc6b9eb9c2edefb62c414dfa85c814690d3",
|
||||
"sha256:b32acd1cdb6202bfe10d9a0858dc463d8960295da70ae18097eb3b85ab12cb91"
|
||||
],
|
||||
"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",
|
||||
@ -60,6 +98,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.60.0"
|
||||
},
|
||||
"azure-mgmt-consumption": {
|
||||
"hashes": [
|
||||
"sha256:035d4b74ca7c47e2683bea17105fd9014c27060336fb6255324ac86b27f70f5b",
|
||||
"sha256:af319ad6e3ec162a7578563f149e3cdd7d833a62ec80761cfd93caf79467610b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"azure-mgmt-managementgroups": {
|
||||
"hashes": [
|
||||
"sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682",
|
||||
@ -208,6 +254,14 @@
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"dataclasses": {
|
||||
"hashes": [
|
||||
"sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836",
|
||||
"sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==0.7"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
|
||||
@ -301,9 +355,9 @@
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"
|
||||
"sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
@ -345,6 +399,20 @@
|
||||
],
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"msal": {
|
||||
"hashes": [
|
||||
"sha256:c944b833bf686dfbc973e9affdef94b77e616cb52ab397e76cde82e26b8a3373",
|
||||
"sha256:ecbe3f5ac77facad16abf08eb9d8562af3bc7184be5d4d90c9ef4db5bde26340"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"msal-extensions": {
|
||||
"hashes": [
|
||||
"sha256:59e171a9a4baacdbf001c66915efeaef372fb424421f1a4397115a3ddd6205dc",
|
||||
"sha256:c5a32b8e1dce1c67733dcdf8aa8bebcff5ab123e779ef7bc14e416bd0da90037"
|
||||
],
|
||||
"version": "==0.1.3"
|
||||
},
|
||||
"msrest": {
|
||||
"hashes": [
|
||||
"sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d",
|
||||
@ -379,6 +447,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.0.5"
|
||||
},
|
||||
"portalocker": {
|
||||
"hashes": [
|
||||
"sha256:6f57aabb25ba176462dc7c63b86c42ad6a9b5bd3d679a9d776d0536bfb803d54",
|
||||
"sha256:dac62e53e5670cb40d2ee4cdc785e6b829665932c3ee75307ad677cf5f7d2e9f"
|
||||
],
|
||||
"version": "==1.5.2"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29",
|
||||
@ -444,6 +519,9 @@
|
||||
"version": "==1.3"
|
||||
},
|
||||
"pyjwt": {
|
||||
"extras": [
|
||||
"crypto"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
||||
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
|
||||
@ -529,17 +607,17 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.13.0"
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec"
|
||||
"sha256:64a7b71846db6423807e96820993fa12a03b89127d278290ca25c0b11ed7b4fb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.12"
|
||||
"version": "==1.3.13"
|
||||
},
|
||||
"sqlalchemy-json": {
|
||||
"hashes": [
|
||||
@ -572,10 +650,10 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
||||
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||
],
|
||||
"version": "==1.25.7"
|
||||
"version": "==1.25.8"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
@ -609,10 +687,10 @@
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
"version": "==2.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -1022,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": [
|
||||
@ -1201,10 +1279,10 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.13.0"
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"smmap2": {
|
||||
"hashes": [
|
||||
@ -1285,10 +1363,10 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
||||
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||
],
|
||||
"version": "==1.25.7"
|
||||
"version": "==1.25.8"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
@ -1319,10 +1397,10 @@
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
"version": "==2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,11 +29,13 @@ parent_dir = Path(__file__).parent.parent
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from atst.app import make_config
|
||||
|
||||
app_config = make_config()
|
||||
config.set_main_option('sqlalchemy.url', app_config['DATABASE_URI'])
|
||||
config.set_main_option("sqlalchemy.url", app_config["DATABASE_URI"])
|
||||
|
||||
from atst.database import db
|
||||
from atst.models import *
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
@ -51,7 +53,8 @@ def run_migrations_offline():
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
@ -66,18 +69,19 @@ def run_migrations_online():
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
connection=connection, target_metadata=target_metadata, compare_type=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
|
132
alembic/versions/26319c44a8d5_state_machine_states_extended.py
Normal file
132
alembic/versions/26319c44a8d5_state_machine_states_extended.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""state machine states extended
|
||||
|
||||
Revision ID: 26319c44a8d5
|
||||
Revises: 59973fa17ded
|
||||
Create Date: 2020-01-22 15:54:03.186751
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "26319c44a8d5" # pragma: allowlist secret
|
||||
down_revision = "59973fa17ded" # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"portfolio_state_machines",
|
||||
"state",
|
||||
existing_type=sa.Enum(
|
||||
"UNSTARTED",
|
||||
"STARTING",
|
||||
"STARTED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"TENANT_CREATED",
|
||||
"TENANT_IN_PROGRESS",
|
||||
"TENANT_FAILED",
|
||||
"BILLING_PROFILE_CREATED",
|
||||
"BILLING_PROFILE_IN_PROGRESS",
|
||||
"BILLING_PROFILE_FAILED",
|
||||
"ADMIN_SUBSCRIPTION_CREATED",
|
||||
"ADMIN_SUBSCRIPTION_IN_PROGRESS",
|
||||
"ADMIN_SUBSCRIPTION_FAILED",
|
||||
name="fsmstates",
|
||||
native_enum=False,
|
||||
),
|
||||
type_=sa.Enum(
|
||||
"UNSTARTED",
|
||||
"STARTING",
|
||||
"STARTED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"TENANT_CREATED",
|
||||
"TENANT_IN_PROGRESS",
|
||||
"TENANT_FAILED",
|
||||
"BILLING_PROFILE_CREATION_CREATED",
|
||||
"BILLING_PROFILE_CREATION_IN_PROGRESS",
|
||||
"BILLING_PROFILE_CREATION_FAILED",
|
||||
"BILLING_PROFILE_VERIFICATION_CREATED",
|
||||
"BILLING_PROFILE_VERIFICATION_IN_PROGRESS",
|
||||
"BILLING_PROFILE_VERIFICATION_FAILED",
|
||||
"BILLING_PROFILE_TENANT_ACCESS_CREATED",
|
||||
"BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS",
|
||||
"BILLING_PROFILE_TENANT_ACCESS_FAILED",
|
||||
"TASK_ORDER_BILLING_CREATION_CREATED",
|
||||
"TASK_ORDER_BILLING_CREATION_IN_PROGRESS",
|
||||
"TASK_ORDER_BILLING_CREATION_FAILED",
|
||||
"TASK_ORDER_BILLING_VERIFICATION_CREATED",
|
||||
"TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS",
|
||||
"TASK_ORDER_BILLING_VERIFICATION_FAILED",
|
||||
"BILLING_INSTRUCTION_CREATED",
|
||||
"BILLING_INSTRUCTION_IN_PROGRESS",
|
||||
"BILLING_INSTRUCTION_FAILED",
|
||||
name="fsmstates",
|
||||
native_enum=False,
|
||||
create_constraint=False,
|
||||
),
|
||||
existing_nullable=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"portfolio_state_machines",
|
||||
"state",
|
||||
existing_type=sa.Enum(
|
||||
"UNSTARTED",
|
||||
"STARTING",
|
||||
"STARTED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"TENANT_CREATED",
|
||||
"TENANT_IN_PROGRESS",
|
||||
"TENANT_FAILED",
|
||||
"BILLING_PROFILE_CREATION_CREATED",
|
||||
"BILLING_PROFILE_CREATION_IN_PROGRESS",
|
||||
"BILLING_PROFILE_CREATION_FAILED",
|
||||
"BILLING_PROFILE_VERIFICATION_CREATED",
|
||||
"BILLING_PROFILE_VERIFICATION_IN_PROGRESS",
|
||||
"BILLING_PROFILE_VERIFICATION_FAILED",
|
||||
"BILLING_PROFILE_TENANT_ACCESS_CREATED",
|
||||
"BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS",
|
||||
"BILLING_PROFILE_TENANT_ACCESS_FAILED",
|
||||
"TASK_ORDER_BILLING_CREATION_CREATED",
|
||||
"TASK_ORDER_BILLING_CREATION_IN_PROGRESS",
|
||||
"TASK_ORDER_BILLING_CREATION_FAILED",
|
||||
"TASK_ORDER_BILLING_VERIFICATION_CREATED",
|
||||
"TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS",
|
||||
"TASK_ORDER_BILLING_VERIFICATION_FAILED",
|
||||
"BILLING_INSTRUCTION_CREATED",
|
||||
"BILLING_INSTRUCTION_IN_PROGRESS",
|
||||
"BILLING_INSTRUCTION_FAILED",
|
||||
name="fsmstates",
|
||||
native_enum=False,
|
||||
),
|
||||
type_=sa.Enum(
|
||||
"UNSTARTED",
|
||||
"STARTING",
|
||||
"STARTED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"TENANT_CREATED",
|
||||
"TENANT_IN_PROGRESS",
|
||||
"TENANT_FAILED",
|
||||
"BILLING_PROFILE_CREATED",
|
||||
"BILLING_PROFILE_IN_PROGRESS",
|
||||
"BILLING_PROFILE_FAILED",
|
||||
"ADMIN_SUBSCRIPTION_CREATED",
|
||||
"ADMIN_SUBSCRIPTION_IN_PROGRESS",
|
||||
"ADMIN_SUBSCRIPTION_FAILED",
|
||||
name="fsmstates",
|
||||
native_enum=False,
|
||||
),
|
||||
existing_nullable=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
@ -34,9 +34,7 @@ def make_csp_provider(app, csp=None):
|
||||
|
||||
|
||||
def _stage_to_classname(stage):
|
||||
return "".join(
|
||||
map(lambda word: word.capitalize(), stage.replace("_", " ").split(" "))
|
||||
)
|
||||
return "".join(map(lambda word: word.capitalize(), stage.split("_")))
|
||||
|
||||
|
||||
def get_stage_csp_class(stage, class_type):
|
||||
@ -45,7 +43,7 @@ def get_stage_csp_class(stage, class_type):
|
||||
class_type is either 'payload' or 'result'
|
||||
|
||||
"""
|
||||
cls_name = "".join([_stage_to_classname(stage), "CSP", class_type.capitalize()])
|
||||
cls_name = f"{_stage_to_classname(stage)}CSP{class_type.capitalize()}"
|
||||
try:
|
||||
return getattr(importlib.import_module("atst.domain.csp.cloud"), cls_name)
|
||||
except AttributeError:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@ from atst.models import (
|
||||
from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.models.utils import claim_for_update
|
||||
from atst.utils.localization import translate
|
||||
|
@ -1,5 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
from flask import current_app as app
|
||||
|
||||
|
||||
class StageStates(Enum):
|
||||
CREATED = "created"
|
||||
@ -9,8 +11,12 @@ class StageStates(Enum):
|
||||
|
||||
class AzureStages(Enum):
|
||||
TENANT = "tenant"
|
||||
BILLING_PROFILE = "billing profile"
|
||||
ADMIN_SUBSCRIPTION = "admin subscription"
|
||||
BILLING_PROFILE_CREATION = "billing profile creation"
|
||||
BILLING_PROFILE_VERIFICATION = "billing profile verification"
|
||||
BILLING_PROFILE_TENANT_ACCESS = "billing profile tenant access"
|
||||
TASK_ORDER_BILLING_CREATION = "task order billing creation"
|
||||
TASK_ORDER_BILLING_VERIFICATION = "task order billing verification"
|
||||
BILLING_INSTRUCTION = "billing instruction"
|
||||
|
||||
|
||||
def _build_csp_states(csp_stages):
|
||||
@ -31,14 +37,14 @@ def _build_csp_states(csp_stages):
|
||||
|
||||
FSMStates = Enum("FSMStates", _build_csp_states(AzureStages))
|
||||
|
||||
compose_state = lambda csp_stage, state: getattr(
|
||||
FSMStates, "_".join([csp_stage.name, state.name])
|
||||
)
|
||||
|
||||
|
||||
def _build_transitions(csp_stages):
|
||||
transitions = []
|
||||
states = []
|
||||
compose_state = lambda csp_stage, state: getattr(
|
||||
FSMStates, "_".join([csp_stage.name, state.name])
|
||||
)
|
||||
|
||||
for stage_i, csp_stage in enumerate(csp_stages):
|
||||
for state in StageStates:
|
||||
states.append(
|
||||
@ -99,6 +105,22 @@ class FSMMixin:
|
||||
{"trigger": "fail", "source": "*", "dest": FSMStates.FAILED,},
|
||||
]
|
||||
|
||||
def fail_stage(self, stage):
|
||||
fail_trigger = "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__()}'"
|
||||
)
|
||||
|
||||
def finish_stage(self, stage):
|
||||
finish_trigger = "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__()}'"
|
||||
)
|
||||
self.trigger(finish_trigger)
|
||||
|
||||
def prepare_init(self, event):
|
||||
pass
|
||||
|
||||
@ -125,13 +147,3 @@ class FSMMixin:
|
||||
|
||||
def after_reset(self, event):
|
||||
pass
|
||||
|
||||
def fail_stage(self, stage):
|
||||
fail_trigger = "fail" + stage
|
||||
if fail_trigger in self.machine.get_triggers(self.current_state.name):
|
||||
self.trigger(fail_trigger)
|
||||
|
||||
def finish_stage(self, stage):
|
||||
finish_trigger = "finish_" + stage
|
||||
if finish_trigger in self.machine.get_triggers(self.current_state.name):
|
||||
self.trigger(finish_trigger)
|
||||
|
@ -50,6 +50,9 @@ class PortfolioStateMachine(
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PortfolioStateMachine(state='{self.current_state.name}', portfolio='{self.portfolio.name}'"
|
||||
|
||||
@reconstructor
|
||||
def attach_machine(self):
|
||||
"""
|
||||
@ -73,103 +76,132 @@ class PortfolioStateMachine(
|
||||
return getattr(FSMStates, self.state)
|
||||
return self.state
|
||||
|
||||
def trigger_next_transition(self):
|
||||
def trigger_next_transition(self, **kwargs):
|
||||
state_obj = self.machine.get_state(self.state)
|
||||
|
||||
if state_obj.is_system:
|
||||
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
||||
# call the first trigger availabe for these two system states
|
||||
trigger_name = self.machine.get_triggers(self.current_state.name)[0]
|
||||
self.trigger(trigger_name)
|
||||
self.trigger(trigger_name, **kwargs)
|
||||
|
||||
elif self.current_state == FSMStates.STARTED:
|
||||
# get the first trigger that starts with 'create_'
|
||||
create_trigger = list(
|
||||
create_trigger = next(
|
||||
filter(
|
||||
lambda trigger: trigger.startswith("create_"),
|
||||
self.machine.get_triggers(FSMStates.STARTED.name),
|
||||
),
|
||||
None,
|
||||
)
|
||||
if create_trigger:
|
||||
self.trigger(create_trigger, **kwargs)
|
||||
else:
|
||||
app.logger.info(
|
||||
f"could not locate 'create trigger' for {self.__repr__()}"
|
||||
)
|
||||
)[0]
|
||||
self.trigger(create_trigger)
|
||||
self.fail_stage(stage)
|
||||
|
||||
elif state_obj.is_IN_PROGRESS:
|
||||
pass
|
||||
elif state_obj.is_CREATED:
|
||||
# the create trigger for the next stage should be in the available
|
||||
# triggers for the current state
|
||||
create_trigger = next(
|
||||
filter(
|
||||
lambda trigger: trigger.startswith("create_"),
|
||||
self.machine.get_triggers(self.state.name),
|
||||
),
|
||||
None,
|
||||
)
|
||||
if create_trigger is not None:
|
||||
self.trigger(create_trigger, **kwargs)
|
||||
|
||||
# elif state_obj.is_TENANT:
|
||||
# pass
|
||||
# elif state_obj.is_BILLING_PROFILE:
|
||||
# pass
|
||||
|
||||
# @with_payload
|
||||
def after_in_progress_callback(self, event):
|
||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||
if stage == "tenant":
|
||||
payload = dict( # nosec
|
||||
creds={"username": "mock-cloud", "pass": "shh"},
|
||||
user_id="123",
|
||||
password="123",
|
||||
domain_name="123",
|
||||
first_name="john",
|
||||
last_name="doe",
|
||||
country_code="US",
|
||||
password_recovery_email_address="password@email.com",
|
||||
)
|
||||
elif stage == "billing_profile":
|
||||
payload = dict(creds={"username": "mock-cloud", "pass": "shh"},)
|
||||
|
||||
# Accumulate payload w/ creds
|
||||
payload = event.kwargs.get("csp_data")
|
||||
payload["creds"] = event.kwargs.get("creds")
|
||||
|
||||
payload_data_cls = get_stage_csp_class(stage, "payload")
|
||||
if not payload_data_cls:
|
||||
app.logger.info(f"could not resolve payload data class for stage {stage}")
|
||||
self.fail_stage(stage)
|
||||
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.info(exc.json())
|
||||
print(exc.json())
|
||||
app.logger.info(payload)
|
||||
self.fail_stage(stage)
|
||||
|
||||
# TODO: Determine best place to do this, maybe @reconstructor
|
||||
csp = event.kwargs.get("csp")
|
||||
if csp is not None:
|
||||
self.csp = AzureCSP(app).cloud
|
||||
else:
|
||||
self.csp = MockCSP(app).cloud
|
||||
|
||||
for attempt in range(5):
|
||||
attempts_count = 5
|
||||
for attempt in range(attempts_count):
|
||||
try:
|
||||
response = getattr(self.csp, "create_" + stage)(payload_data)
|
||||
func_name = f"create_{stage}"
|
||||
response = getattr(self.csp, func_name)(payload_data)
|
||||
except (ConnectionException, UnknownServerException) as exc:
|
||||
print("caught exception. retry", attempt)
|
||||
app.logger.error(
|
||||
f"CSP api call. Caught exception for {self.__repr__()}. Retry attempt {attempt}",
|
||||
exc_info=1,
|
||||
)
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
# failed all attempts
|
||||
logger.info(f"CSP api call failed after {attempts_count} attempts.")
|
||||
self.fail_stage(stage)
|
||||
|
||||
if self.portfolio.csp_data is None:
|
||||
self.portfolio.csp_data = {}
|
||||
self.portfolio.csp_data[stage + "_data"] = response
|
||||
self.portfolio.csp_data.update(response)
|
||||
db.session.add(self.portfolio)
|
||||
db.session.commit()
|
||||
|
||||
# store any updated creds, if necessary
|
||||
|
||||
self.finish_stage(stage)
|
||||
|
||||
def is_csp_data_valid(self, event):
|
||||
# check portfolio csp details json field for fields
|
||||
|
||||
if self.portfolio.csp_data is None or not isinstance(
|
||||
self.portfolio.csp_data, dict
|
||||
):
|
||||
print("no csp data")
|
||||
return False
|
||||
|
||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||
stage_data = self.portfolio.csp_data.get(stage + "_data")
|
||||
stage_data = self.portfolio.csp_data
|
||||
cls = get_stage_csp_class(stage, "result")
|
||||
if not cls:
|
||||
return False
|
||||
|
||||
try:
|
||||
cls(**stage_data)
|
||||
dc = cls(**stage_data)
|
||||
if getattr(dc, "get_creds", None) is not None:
|
||||
new_creds = dc.get_creds()
|
||||
tenant_id = new_creds.get("tenant_id")
|
||||
secret = self.csp.get_secret(tenant_id)
|
||||
secret.update(new_creds)
|
||||
self.csp.set_secret(tenant_id, secret)
|
||||
|
||||
except PydanticValidationError as exc:
|
||||
print(exc.json())
|
||||
app.logger.error(
|
||||
f"Payload Validation Error in {self.__repr__()}:", exc_info=1
|
||||
)
|
||||
app.logger.info(exc.json())
|
||||
app.logger.info(payload)
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -25,6 +25,11 @@ def camel_to_snake(camel_cased):
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||
|
||||
|
||||
def snake_to_camel(snake_cased):
|
||||
parts = snake_cased.split("_")
|
||||
return f"{parts[0]}{''.join([w.capitalize() for w in parts[1:]])}"
|
||||
|
||||
|
||||
def pick(keys, dct):
|
||||
_keys = set(keys)
|
||||
return {k: v for (k, v) in dct.items() if k in _keys}
|
||||
|
@ -1,22 +1,37 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.domain.csp.cloud import AzureCloudProvider
|
||||
from atst.domain.csp.cloud import (
|
||||
AzureCloudProvider,
|
||||
BillingProfileCreationCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
BillingProfileTenantAccessCSPPayload,
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantCSPPayload,
|
||||
TenantCSPResult,
|
||||
)
|
||||
|
||||
from tests.mock_azure import mock_azure, AUTH_CREDENTIALS
|
||||
from tests.factories import EnvironmentFactory, ApplicationFactory
|
||||
|
||||
|
||||
# TODO: Directly test create subscription, provide all args √
|
||||
# TODO: Test create environment (create management group with parent)
|
||||
# TODO: Test create application (create manageemnt group with parent)
|
||||
# Create reusable mock for mocking the management group calls for multiple services
|
||||
#
|
||||
creds = {
|
||||
"home_tenant_id": "tenant_id",
|
||||
"client_id": "client_id",
|
||||
"secret_key": "secret_key",
|
||||
}
|
||||
BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment = EnvironmentFactory.create()
|
||||
|
||||
@ -51,14 +66,12 @@ def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result == subscription_id
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
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 = Mock(
|
||||
**spec_dict
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment = EnvironmentFactory.create()
|
||||
|
||||
@ -71,7 +84,6 @@ def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result.id == "Test Id"
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||
application = ApplicationFactory.create()
|
||||
|
||||
@ -82,7 +94,6 @@ def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result.id == "Test Id"
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment_id = str(uuid4())
|
||||
|
||||
@ -97,7 +108,6 @@ def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result.get("csp_user_id") == csp_user_id
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
||||
subscription_id = str(uuid4())
|
||||
management_group_id = str(uuid4())
|
||||
@ -121,3 +131,287 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
||||
policy_definition_name=properties.get("displayName"),
|
||||
parameters=mock_policy_definition,
|
||||
)
|
||||
|
||||
|
||||
def test_create_tenant(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.json.return_value = {
|
||||
"objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
|
||||
"tenantId": "60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
"userId": "1153801116406515559",
|
||||
}
|
||||
mock_result.status_code = 200
|
||||
mock_azure.sdk.requests.post.return_value = mock_result
|
||||
payload = TenantCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
user_id="admin",
|
||||
password="JediJan13$coot",
|
||||
domain_name="jediccpospawnedtenant2",
|
||||
first_name="Tedry",
|
||||
last_name="Tenet",
|
||||
country_code="US",
|
||||
password_recovery_email_address="thomas@promptworks.com",
|
||||
)
|
||||
)
|
||||
result = mock_azure.create_tenant(payload)
|
||||
body: TenantCSPResult = result.get("body")
|
||||
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
|
||||
|
||||
|
||||
def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.headers = {
|
||||
"Location": "http://retry-url",
|
||||
"Retry-After": "10",
|
||||
}
|
||||
mock_result.status_code = 202
|
||||
mock_azure.sdk.requests.post.return_value = mock_result
|
||||
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",
|
||||
),
|
||||
creds=creds,
|
||||
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
billing_profile_display_name="Test Billing Profile",
|
||||
billing_account_name=BILLING_ACCOUNT_NAME,
|
||||
)
|
||||
)
|
||||
result = mock_azure.create_billing_profile_creation(payload)
|
||||
body: BillingProfileCreationCSPResult = result.get("body")
|
||||
assert body.billing_profile_retry_after == 10
|
||||
|
||||
|
||||
def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.status_code = 200
|
||||
mock_result.json.return_value = {
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB",
|
||||
"name": "KQWI-W2SU-BG7-TGB",
|
||||
"properties": {
|
||||
"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": [],
|
||||
"hasReadAccess": True,
|
||||
"invoiceDay": 5,
|
||||
"invoiceEmailOptIn": False,
|
||||
"invoiceSections": [
|
||||
{
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/invoiceSections/6HMZ-2HLO-PJA-TGB",
|
||||
"name": "6HMZ-2HLO-PJA-TGB",
|
||||
"properties": {"displayName": "First Portfolio Billing Profile"},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections",
|
||||
}
|
||||
],
|
||||
},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles",
|
||||
}
|
||||
mock_azure.sdk.requests.get.return_value = mock_result
|
||||
|
||||
payload = BillingProfileVerificationCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
billing_profile_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
|
||||
)
|
||||
)
|
||||
|
||||
result = mock_azure.create_billing_profile_verification(payload)
|
||||
body: BillingProfileVerificationCSPResult = result.get("body")
|
||||
assert body.billing_profile_name == "KQWI-W2SU-BG7-TGB"
|
||||
assert (
|
||||
body.billing_profile_properties.billing_profile_display_name
|
||||
== "First Portfolio Billing Profile"
|
||||
)
|
||||
|
||||
|
||||
def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.status_code = 201
|
||||
mock_result.json.return_value = {
|
||||
"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",
|
||||
}
|
||||
|
||||
mock_azure.sdk.requests.post.return_value = mock_result
|
||||
|
||||
payload = BillingProfileTenantAccessCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
|
||||
billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
|
||||
billing_profile_name="KQWI-W2SU-BG7-TGB",
|
||||
)
|
||||
)
|
||||
|
||||
result = mock_azure.create_billing_profile_tenant_access(payload)
|
||||
body: BillingProfileTenantAccessCSPResult = result.get("body")
|
||||
assert (
|
||||
body.billing_role_assignment_name
|
||||
== "40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d"
|
||||
)
|
||||
|
||||
|
||||
def test_create_task_order_billing_creation(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.status_code = 202
|
||||
mock_result.headers = {
|
||||
"Location": "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/patchBillingProfile_KQWI-W2SU-BG7-TGB:02715576-4118-466c-bca7-b1cd3169ff46?api-version=2019-10-01-preview",
|
||||
"Retry-After": "10",
|
||||
}
|
||||
|
||||
mock_azure.sdk.requests.patch.return_value = mock_result
|
||||
|
||||
payload = TaskOrderBillingCreationCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
|
||||
billing_profile_name="KQWI-W2SU-BG7-TGB",
|
||||
)
|
||||
)
|
||||
|
||||
result = mock_azure.create_task_order_billing_creation(payload)
|
||||
body: TaskOrderBillingCreationCSPResult = result.get("body")
|
||||
assert (
|
||||
body.task_order_billing_verify_url
|
||||
== "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/patchBillingProfile_KQWI-W2SU-BG7-TGB:02715576-4118-466c-bca7-b1cd3169ff46?api-version=2019-10-01-preview"
|
||||
)
|
||||
|
||||
|
||||
def test_create_task_order_billing_verification(mock_azure):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.status_code = 200
|
||||
mock_result.json.return_value = {
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB",
|
||||
"name": "KQWI-W2SU-BG7-TGB",
|
||||
"properties": {
|
||||
"address": {
|
||||
"addressLine1": "123 S Broad Street, Suite 2400",
|
||||
"city": "Philadelphia",
|
||||
"companyName": "Promptworks",
|
||||
"country": "US",
|
||||
"postalCode": "19109",
|
||||
"region": "PA",
|
||||
},
|
||||
"currency": "USD",
|
||||
"displayName": "Test Billing Profile",
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"productId": "DZH318Z0BPS6",
|
||||
"skuId": "0001",
|
||||
"skuDescription": "Microsoft Azure Plan",
|
||||
}
|
||||
],
|
||||
"hasReadAccess": True,
|
||||
"invoiceDay": 5,
|
||||
"invoiceEmailOptIn": False,
|
||||
"invoiceSections": [
|
||||
{
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/invoiceSections/CHCO-BAAR-PJA-TGB",
|
||||
"name": "CHCO-BAAR-PJA-TGB",
|
||||
"properties": {"displayName": "Test Billing Profile"},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections",
|
||||
}
|
||||
],
|
||||
},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles",
|
||||
}
|
||||
mock_azure.sdk.requests.get.return_value = mock_result
|
||||
|
||||
payload = TaskOrderBillingVerificationCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
task_order_billing_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
|
||||
)
|
||||
)
|
||||
|
||||
result = mock_azure.create_task_order_billing_verification(payload)
|
||||
body: TaskOrderBillingVerificationCSPResult = result.get("body")
|
||||
assert body.billing_profile_name == "KQWI-W2SU-BG7-TGB"
|
||||
assert (
|
||||
body.billing_profile_enabled_plan_details.enabled_azure_plans[0].get("skuId")
|
||||
== "0001"
|
||||
)
|
||||
|
||||
|
||||
def test_create_billing_instruction(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.status_code = 200
|
||||
mock_result.json.return_value = {
|
||||
"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",
|
||||
}
|
||||
|
||||
mock_azure.sdk.requests.put.return_value = mock_result
|
||||
|
||||
payload = BillingInstructionCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
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",
|
||||
)
|
||||
)
|
||||
result = mock_azure.create_billing_instruction(payload)
|
||||
body: BillingInstructionCSPResult = result.get("body")
|
||||
assert body.reported_clin_name == "TO1:CLIN001"
|
||||
|
@ -1,16 +1,21 @@
|
||||
import pytest
|
||||
import re
|
||||
|
||||
from tests.factories import (
|
||||
PortfolioFactory,
|
||||
PortfolioStateMachineFactory,
|
||||
CLINFactory,
|
||||
)
|
||||
|
||||
from atst.models import FSMStates
|
||||
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()
|
||||
# TODO: setup clin/to as active/funded/ready
|
||||
portfolio = CLINFactory.create().task_order.portfolio
|
||||
return portfolio
|
||||
|
||||
|
||||
@ -19,18 +24,130 @@ def test_fsm_creation(portfolio):
|
||||
assert sm.portfolio
|
||||
|
||||
|
||||
def test_fsm_transition_start(portfolio):
|
||||
def test_state_machine_trigger_next_transition(portfolio):
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
|
||||
sm.trigger_next_transition()
|
||||
assert sm.current_state == FSMStates.STARTING
|
||||
|
||||
sm.trigger_next_transition()
|
||||
assert sm.current_state == FSMStates.STARTED
|
||||
|
||||
|
||||
def test_state_machine_compose_state(portfolio):
|
||||
PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
assert (
|
||||
compose_state(AzureStages.TENANT, StageStates.CREATED)
|
||||
== FSMStates.TENANT_CREATED
|
||||
)
|
||||
|
||||
|
||||
def test_state_machine_valid_data_classes_for_stages(portfolio):
|
||||
PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
for stage in AzureStages:
|
||||
assert get_stage_csp_class(stage.name.lower(), "payload") is not None
|
||||
assert get_stage_csp_class(stage.name.lower(), "result") is not None
|
||||
|
||||
|
||||
def test_state_machine_initialization(portfolio):
|
||||
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
for stage in AzureStages:
|
||||
|
||||
# check that all stages have a 'create' and 'fail' triggers
|
||||
stage_name = stage.name.lower()
|
||||
for trigger_prefix in ["create", "fail"]:
|
||||
assert hasattr(sm, trigger_prefix + "_" + stage_name)
|
||||
|
||||
# check that machine
|
||||
in_progress_triggers = sm.machine.get_triggers(stage.name + "_IN_PROGRESS")
|
||||
assert [
|
||||
"reset",
|
||||
"fail",
|
||||
"finish_" + stage_name,
|
||||
"fail_" + stage_name,
|
||||
] == in_progress_triggers
|
||||
|
||||
started_triggers = sm.machine.get_triggers("STARTED")
|
||||
create_trigger = next(
|
||||
filter(
|
||||
lambda trigger: trigger.startswith("create_"),
|
||||
sm.machine.get_triggers(FSMStates.STARTED.name),
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert ["reset", "fail", create_trigger] == started_triggers
|
||||
|
||||
|
||||
def test_fsm_transition_start(portfolio: Portfolio):
|
||||
sm: PortfolioStateMachine = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
assert sm.portfolio
|
||||
assert sm.state == FSMStates.UNSTARTED
|
||||
|
||||
# next_state does not create the trigger callbacks !!!
|
||||
# sm.next_state()
|
||||
|
||||
sm.init()
|
||||
assert sm.state == FSMStates.STARTING
|
||||
|
||||
sm.start()
|
||||
assert sm.state == FSMStates.STARTED
|
||||
sm.create_tenant(a=1, b=2)
|
||||
assert sm.state == FSMStates.TENANT_CREATED
|
||||
|
||||
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:
|
||||
csp_data = portfolio.csp_data
|
||||
else:
|
||||
csp_data = {}
|
||||
|
||||
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": ppoc.first_name,
|
||||
"last_name": ppoc.last_name,
|
||||
"country_code": "US",
|
||||
"password_recovery_email_address": ppoc.email,
|
||||
"address": {
|
||||
"company_name": "",
|
||||
"address_line_1": "",
|
||||
"city": "",
|
||||
"region": "",
|
||||
"country": "",
|
||||
"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"}
|
||||
|
||||
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 = {}
|
||||
|
@ -8,6 +8,7 @@ AZURE_CONFIG = {
|
||||
"AZURE_SECRET_KEY": "MOCK",
|
||||
"AZURE_TENANT_ID": "MOCK",
|
||||
"AZURE_POLICY_LOCATION": "policies",
|
||||
"AZURE_VAULT_URL": "http://vault",
|
||||
}
|
||||
|
||||
AUTH_CREDENTIALS = {
|
||||
@ -53,16 +54,38 @@ def mock_policy():
|
||||
return Mock(spec=policy)
|
||||
|
||||
|
||||
def mock_adal():
|
||||
import adal
|
||||
|
||||
return Mock(spec=adal)
|
||||
|
||||
|
||||
def mock_requests():
|
||||
import 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
|
||||
|
||||
self.subscription = mock_subscription()
|
||||
self.authorization = mock_authorization()
|
||||
self.policy = mock_policy()
|
||||
self.adal = mock_adal()
|
||||
self.managementgroups = mock_managementgroups()
|
||||
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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user