Merge branch 'staging' into funding-alert
This commit is contained in:
commit
1310434243
4
Pipfile
4
Pipfile
@ -33,6 +33,10 @@ azure-mgmt-authorization = "*"
|
|||||||
azure-mgmt-managementgroups = "*"
|
azure-mgmt-managementgroups = "*"
|
||||||
azure-mgmt-resource = "*"
|
azure-mgmt-resource = "*"
|
||||||
transitions = "*"
|
transitions = "*"
|
||||||
|
azure-mgmt-consumption = "*"
|
||||||
|
adal = "*"
|
||||||
|
azure-identity = "*"
|
||||||
|
azure-keyvault = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
|
134
Pipfile.lock
generated
134
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "63b8f9d203f306a6f0ff20514b024909aa7e64917e1befcc9ea79931b5b4bd34"
|
"sha256": "4dbb023bcb860eb6dc56e1c201c91f272e1e67ad03e5e5eeb3a7a7fdff350eed"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -21,14 +21,15 @@
|
|||||||
"sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf",
|
"sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf",
|
||||||
"sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1"
|
"sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1"
|
||||||
],
|
],
|
||||||
|
"index": "pypi",
|
||||||
"version": "==1.2.2"
|
"version": "==1.2.2"
|
||||||
},
|
},
|
||||||
"alembic": {
|
"alembic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d"
|
"sha256:d412982920653db6e5a44bfd13b1d0db5685cbaaccaf226195749c706e1e862a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.2"
|
"version": "==1.3.3"
|
||||||
},
|
},
|
||||||
"amqp": {
|
"amqp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -44,6 +45,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.1.24"
|
"version": "==1.1.24"
|
||||||
},
|
},
|
||||||
|
"azure-core": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b8ccbd901d085048e4e3e72627b066923c5bd3780e4c43cf9cf9948aee9bdf9e",
|
||||||
|
"sha256:e2cd99f0c0aef12c168d498cb5bc47a3a45c8ab08112183e3ec97e4dcb33ceb9"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
"azure-graphrbac": {
|
"azure-graphrbac": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:53e98ae2ca7c19b349e9e9bb1b6a824aeae8dcfcbe17190d20fe69c0f185b2e2",
|
"sha256:53e98ae2ca7c19b349e9e9bb1b6a824aeae8dcfcbe17190d20fe69c0f185b2e2",
|
||||||
@ -52,6 +60,36 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.61.1"
|
"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": {
|
"azure-mgmt-authorization": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:31e875a34ac2c5d6fefe77b4a8079a8b2bdbe9edb957e47e8b44222fb212d6a7",
|
"sha256:31e875a34ac2c5d6fefe77b4a8079a8b2bdbe9edb957e47e8b44222fb212d6a7",
|
||||||
@ -60,6 +98,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.60.0"
|
"version": "==0.60.0"
|
||||||
},
|
},
|
||||||
|
"azure-mgmt-consumption": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:035d4b74ca7c47e2683bea17105fd9014c27060336fb6255324ac86b27f70f5b",
|
||||||
|
"sha256:af319ad6e3ec162a7578563f149e3cdd7d833a62ec80761cfd93caf79467610b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
"azure-mgmt-managementgroups": {
|
"azure-mgmt-managementgroups": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682",
|
"sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682",
|
||||||
@ -208,6 +254,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.8"
|
"version": "==2.8"
|
||||||
},
|
},
|
||||||
|
"dataclasses": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836",
|
||||||
|
"sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.7'",
|
||||||
|
"version": "==0.7"
|
||||||
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
|
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
|
||||||
@ -301,9 +355,9 @@
|
|||||||
},
|
},
|
||||||
"mako": {
|
"mako": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"
|
"sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"
|
||||||
],
|
],
|
||||||
"version": "==1.1.0"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -345,6 +399,20 @@
|
|||||||
],
|
],
|
||||||
"version": "==8.1.0"
|
"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": {
|
"msrest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d",
|
"sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d",
|
||||||
@ -379,6 +447,13 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.0.5"
|
"version": "==2.0.5"
|
||||||
},
|
},
|
||||||
|
"portalocker": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6f57aabb25ba176462dc7c63b86c42ad6a9b5bd3d679a9d776d0536bfb803d54",
|
||||||
|
"sha256:dac62e53e5670cb40d2ee4cdc785e6b829665932c3ee75307ad677cf5f7d2e9f"
|
||||||
|
],
|
||||||
|
"version": "==1.5.2"
|
||||||
|
},
|
||||||
"psycopg2-binary": {
|
"psycopg2-binary": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29",
|
"sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29",
|
||||||
@ -444,6 +519,9 @@
|
|||||||
"version": "==1.3"
|
"version": "==1.3"
|
||||||
},
|
},
|
||||||
"pyjwt": {
|
"pyjwt": {
|
||||||
|
"extras": [
|
||||||
|
"crypto"
|
||||||
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
||||||
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
|
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
|
||||||
@ -529,17 +607,17 @@
|
|||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||||
],
|
],
|
||||||
"version": "==1.13.0"
|
"version": "==1.14.0"
|
||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec"
|
"sha256:64a7b71846db6423807e96820993fa12a03b89127d278290ca25c0b11ed7b4fb"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.12"
|
"version": "==1.3.13"
|
||||||
},
|
},
|
||||||
"sqlalchemy-json": {
|
"sqlalchemy-json": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -572,10 +650,10 @@
|
|||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||||
],
|
],
|
||||||
"version": "==1.25.7"
|
"version": "==1.25.8"
|
||||||
},
|
},
|
||||||
"vine": {
|
"vine": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -609,10 +687,10 @@
|
|||||||
},
|
},
|
||||||
"zipp": {
|
"zipp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
"sha256:e013e7800f60ec4dde789ebf4e9f7a54236e4bbf5df2a1a4e20ce9e1d9609d67"
|
||||||
],
|
],
|
||||||
"version": "==1.0.0"
|
"version": "==2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
@ -1022,11 +1100,11 @@
|
|||||||
},
|
},
|
||||||
"pexpect": {
|
"pexpect": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
|
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
|
||||||
"sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
|
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
|
||||||
],
|
],
|
||||||
"markers": "sys_platform != 'win32'",
|
"markers": "sys_platform != 'win32'",
|
||||||
"version": "==4.7.0"
|
"version": "==4.8.0"
|
||||||
},
|
},
|
||||||
"pickleshare": {
|
"pickleshare": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1201,10 +1279,10 @@
|
|||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||||
],
|
],
|
||||||
"version": "==1.13.0"
|
"version": "==1.14.0"
|
||||||
},
|
},
|
||||||
"smmap2": {
|
"smmap2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1285,10 +1363,10 @@
|
|||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||||
],
|
],
|
||||||
"version": "==1.25.7"
|
"version": "==1.25.8"
|
||||||
},
|
},
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1319,10 +1397,10 @@
|
|||||||
},
|
},
|
||||||
"zipp": {
|
"zipp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
"sha256:b338014b9bc7102ca69e0fb96ed07215a8954d2989bc5d83658494ab2ba634af",
|
||||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
"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)
|
sys.path.append(parent_dir)
|
||||||
|
|
||||||
from atst.app import make_config
|
from atst.app import make_config
|
||||||
|
|
||||||
app_config = 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.database import db
|
||||||
from atst.models import *
|
from atst.models import *
|
||||||
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +53,8 @@ def run_migrations_offline():
|
|||||||
"""
|
"""
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
context.configure(
|
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():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
@ -66,18 +69,19 @@ def run_migrations_online():
|
|||||||
"""
|
"""
|
||||||
connectable = engine_from_config(
|
connectable = engine_from_config(
|
||||||
config.get_section(config.config_ini_section),
|
config.get_section(config.config_ini_section),
|
||||||
prefix='sqlalchemy.',
|
prefix="sqlalchemy.",
|
||||||
poolclass=pool.NullPool)
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection,
|
connection=connection, target_metadata=target_metadata, compare_type=True
|
||||||
target_metadata=target_metadata
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
if context.is_offline_mode():
|
||||||
run_migrations_offline()
|
run_migrations_offline()
|
||||||
else:
|
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 ###
|
@ -1,5 +1,3 @@
|
|||||||
import importlib
|
|
||||||
|
|
||||||
from .cloud import MockCloudProvider
|
from .cloud import MockCloudProvider
|
||||||
from .file_uploads import AzureUploader, MockUploader
|
from .file_uploads import AzureUploader, MockUploader
|
||||||
from .reports import MockReportingProvider
|
from .reports import MockReportingProvider
|
||||||
@ -31,22 +29,3 @@ def make_csp_provider(app, csp=None):
|
|||||||
app.csp = MockCSP(app, test_mode=True)
|
app.csp = MockCSP(app, test_mode=True)
|
||||||
else:
|
else:
|
||||||
app.csp = MockCSP(app)
|
app.csp = MockCSP(app)
|
||||||
|
|
||||||
|
|
||||||
def _stage_to_classname(stage):
|
|
||||||
return "".join(
|
|
||||||
map(lambda word: word.capitalize(), stage.replace("_", " ").split(" "))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_stage_csp_class(stage, class_type):
|
|
||||||
"""
|
|
||||||
given a stage name and class_type return the class
|
|
||||||
class_type is either 'payload' or 'result'
|
|
||||||
|
|
||||||
"""
|
|
||||||
cls_name = "".join([_stage_to_classname(stage), "CSP", class_type.capitalize()])
|
|
||||||
try:
|
|
||||||
return getattr(importlib.import_module("atst.domain.csp.cloud"), cls_name)
|
|
||||||
except AttributeError:
|
|
||||||
print("could not import CSP Result class <%s>" % cls_name)
|
|
||||||
|
@ -1,889 +0,0 @@
|
|||||||
import re
|
|
||||||
from typing import Dict
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from atst.models.user import User
|
|
||||||
from atst.models.environment import Environment
|
|
||||||
from atst.models.environment_role import EnvironmentRole
|
|
||||||
|
|
||||||
|
|
||||||
class GeneralCSPException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OperationInProgressException(GeneralCSPException):
|
|
||||||
"""Throw this for instances when the CSP reports that the current entity is already
|
|
||||||
being operated on/created/deleted/etc
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, operation_desc):
|
|
||||||
self.operation_desc = operation_desc
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "An operation for this entity is already in progress: {}".format(
|
|
||||||
self.operation_desc
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationException(GeneralCSPException):
|
|
||||||
"""Throw this for instances when there is a problem with the auth credentials:
|
|
||||||
* Missing credentials
|
|
||||||
* Incorrect credentials
|
|
||||||
* Other credential problems
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, auth_error):
|
|
||||||
self.auth_error = auth_error
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "An error occurred with authentication: {}".format(self.auth_error)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationException(GeneralCSPException):
|
|
||||||
"""Throw this for instances when the current credentials are not authorized
|
|
||||||
for the current action.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, auth_error):
|
|
||||||
self.auth_error = auth_error
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "An error occurred with authorization: {}".format(self.auth_error)
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionException(GeneralCSPException):
|
|
||||||
"""A general problem with the connection, timeouts or unresolved endpoints
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, connection_error):
|
|
||||||
self.connection_error = connection_error
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "Could not connect to cloud provider: {}".format(self.connection_error)
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownServerException(GeneralCSPException):
|
|
||||||
"""An error occured on the CSP side (5xx) and we don't know why
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, server_error):
|
|
||||||
self.server_error = server_error
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "A server error occured: {}".format(self.server_error)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentCreationException(GeneralCSPException):
|
|
||||||
"""If there was an error in creating the environment
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, env_identifier, reason):
|
|
||||||
self.env_identifier = env_identifier
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "The envionment {} couldn't be created: {}".format(
|
|
||||||
self.env_identifier, self.reason
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserProvisioningException(GeneralCSPException):
|
|
||||||
"""Failed to provision a user
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, env_identifier, user_identifier, reason):
|
|
||||||
self.env_identifier = env_identifier
|
|
||||||
self.user_identifier = user_identifier
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "Failed to create user {} for environment {}: {}".format(
|
|
||||||
self.user_identifier, self.env_identifier, self.reason
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserRemovalException(GeneralCSPException):
|
|
||||||
"""Failed to remove a user
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, user_csp_id, reason):
|
|
||||||
self.user_csp_id = user_csp_id
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "Failed to suspend or delete user {}: {}".format(
|
|
||||||
self.user_csp_id, self.reason
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaselineProvisionException(GeneralCSPException):
|
|
||||||
"""If there's any issues standing up whatever is required
|
|
||||||
for an environment baseline
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, env_identifier, reason):
|
|
||||||
self.env_identifier = env_identifier
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "Could not complete baseline provisioning for environment ({}): {}".format(
|
|
||||||
self.env_identifier, self.reason
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCSPPayload(BaseModel):
|
|
||||||
# {"username": "mock-cloud", "pass": "shh"}
|
|
||||||
creds: Dict
|
|
||||||
|
|
||||||
|
|
||||||
class TenantCSPPayload(BaseCSPPayload):
|
|
||||||
user_id: str
|
|
||||||
password: str
|
|
||||||
domain_name: str
|
|
||||||
first_name: str
|
|
||||||
last_name: str
|
|
||||||
country_code: str
|
|
||||||
password_recovery_email_address: str
|
|
||||||
|
|
||||||
|
|
||||||
class TenantCSPResult(BaseModel):
|
|
||||||
user_id: str
|
|
||||||
tenant_id: str
|
|
||||||
user_object_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class BillingProfileAddress(BaseModel):
|
|
||||||
address: Dict
|
|
||||||
"""
|
|
||||||
"address": {
|
|
||||||
"firstName": "string",
|
|
||||||
"lastName": "string",
|
|
||||||
"companyName": "string",
|
|
||||||
"addressLine1": "string",
|
|
||||||
"addressLine2": "string",
|
|
||||||
"addressLine3": "string",
|
|
||||||
"city": "string",
|
|
||||||
"region": "string",
|
|
||||||
"country": "string",
|
|
||||||
"postalCode": "string"
|
|
||||||
},
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BillingProfileCLINBudget(BaseModel):
|
|
||||||
clinBudget: Dict
|
|
||||||
"""
|
|
||||||
"clinBudget": {
|
|
||||||
"amount": 0,
|
|
||||||
"startDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"endDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"externalReferenceId": "string"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BillingProfileCSPPayload(
|
|
||||||
BaseCSPPayload, BillingProfileAddress, BillingProfileCLINBudget
|
|
||||||
):
|
|
||||||
displayName: str
|
|
||||||
poNumber: str
|
|
||||||
invoiceEmailOptIn: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"displayName": "string",
|
|
||||||
"poNumber": "string",
|
|
||||||
"address": {
|
|
||||||
"firstName": "string",
|
|
||||||
"lastName": "string",
|
|
||||||
"companyName": "string",
|
|
||||||
"addressLine1": "string",
|
|
||||||
"addressLine2": "string",
|
|
||||||
"addressLine3": "string",
|
|
||||||
"city": "string",
|
|
||||||
"region": "string",
|
|
||||||
"country": "string",
|
|
||||||
"postalCode": "string"
|
|
||||||
},
|
|
||||||
"invoiceEmailOptIn": true,
|
|
||||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
|
||||||
"enabledAzurePlans": [
|
|
||||||
{
|
|
||||||
"skuId": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"clinBudget": {
|
|
||||||
"amount": 0,
|
|
||||||
"startDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"endDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"externalReferenceId": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class CloudProviderInterface:
|
|
||||||
def root_creds(self) -> Dict:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_environment(
|
|
||||||
self, auth_credentials: Dict, user: User, environment: Environment
|
|
||||||
) -> str:
|
|
||||||
"""Create a new environment in the CSP.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
auth_credentials -- Object containing CSP account credentials
|
|
||||||
user -- ATAT user authorizing the environment creation
|
|
||||||
environment -- ATAT Environment model
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
string: ID of created environment
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationException: Problem with the credentials
|
|
||||||
AuthorizationException: Credentials not authorized for current action(s)
|
|
||||||
ConnectionException: Issue with the CSP API connection
|
|
||||||
UnknownServerException: Unknown issue on the CSP side
|
|
||||||
EnvironmentExistsException: Environment already exists and has been created
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_atat_admin_user(
|
|
||||||
self, auth_credentials: Dict, csp_environment_id: str
|
|
||||||
) -> Dict:
|
|
||||||
"""Creates a new, programmatic user in the CSP. Grants this user full permissions to administer
|
|
||||||
the CSP.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
auth_credentials -- Object containing CSP account credentials
|
|
||||||
csp_environment_id -- ID of the CSP Environment the admin user should be created in
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
object: Object representing new remote admin user, including credentials
|
|
||||||
Something like:
|
|
||||||
{
|
|
||||||
"user_id": string,
|
|
||||||
"credentials": dict, # structure TBD based on csp
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationException: Problem with the credentials
|
|
||||||
AuthorizationException: Credentials not authorized for current action(s)
|
|
||||||
ConnectionException: Issue with the CSP API connection
|
|
||||||
UnknownServerException: Unknown issue on the CSP side
|
|
||||||
UserProvisioningException: Problem creating the root user
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_or_update_user(
|
|
||||||
self, auth_credentials: Dict, user_info: EnvironmentRole, csp_role_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Creates a user or updates an existing user's role.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
auth_credentials -- Object containing CSP account credentials
|
|
||||||
user_info -- instance of EnvironmentRole containing user data
|
|
||||||
if it has a csp_user_id it will try to update that user
|
|
||||||
csp_role_id -- The id of the role the user should be given in the CSP
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
string: Returns the interal csp_user_id of the created/updated user account
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationException: Problem with the credentials
|
|
||||||
AuthorizationException: Credentials not authorized for current action(s)
|
|
||||||
ConnectionException: Issue with the CSP API connection
|
|
||||||
UnknownServerException: Unknown issue on the CSP side
|
|
||||||
UserProvisioningException: User couldn't be created or modified
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def disable_user(self, auth_credentials: Dict, csp_user_id: str) -> bool:
|
|
||||||
"""Revoke all privileges for a user. Used to prevent user access while a full
|
|
||||||
delete is being processed.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
auth_credentials -- Object containing CSP account credentials
|
|
||||||
csp_user_id -- CSP internal user identifier
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool -- True on success
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationException: Problem with the credentials
|
|
||||||
AuthorizationException: Credentials not authorized for current action(s)
|
|
||||||
ConnectionException: Issue with the CSP API connection
|
|
||||||
UnknownServerException: Unknown issue on the CSP side
|
|
||||||
UserRemovalException: User couldn't be suspended
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def get_calculator_url(self) -> str:
|
|
||||||
"""Returns the calculator url for the CSP.
|
|
||||||
This will likely be a static property elsewhere once a CSP is chosen.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def get_environment_login_url(self, environment) -> str:
|
|
||||||
"""Returns the login url for a given environment
|
|
||||||
This may move to be a computed property on the Environment domain object
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_subscription(self, environment):
|
|
||||||
"""Returns True if a new subscription has been created or raises an
|
|
||||||
exception if an error occurs while creating a subscription.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class MockCloudProvider(CloudProviderInterface):
|
|
||||||
|
|
||||||
# TODO: All of these constants
|
|
||||||
AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.")
|
|
||||||
AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.")
|
|
||||||
NETWORK_EXCEPTION = ConnectionException("Network failure.")
|
|
||||||
SERVER_EXCEPTION = UnknownServerException("Not our fault.")
|
|
||||||
|
|
||||||
SERVER_FAILURE_PCT = 1
|
|
||||||
NETWORK_FAILURE_PCT = 7
|
|
||||||
ENV_CREATE_FAILURE_PCT = 12
|
|
||||||
ATAT_ADMIN_CREATE_FAILURE_PCT = 12
|
|
||||||
UNAUTHORIZED_RATE = 2
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, config, with_delay=True, with_failure=True, with_authorization=True
|
|
||||||
):
|
|
||||||
from time import sleep
|
|
||||||
import random
|
|
||||||
|
|
||||||
self._with_delay = with_delay
|
|
||||||
self._with_failure = with_failure
|
|
||||||
self._with_authorization = with_authorization
|
|
||||||
self._sleep = sleep
|
|
||||||
self._random = random
|
|
||||||
|
|
||||||
def root_creds(self):
|
|
||||||
return self._auth_credentials
|
|
||||||
|
|
||||||
def create_environment(self, auth_credentials, user, environment):
|
|
||||||
self._authorize(auth_credentials)
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ENV_CREATE_FAILURE_PCT,
|
|
||||||
EnvironmentCreationException(
|
|
||||||
environment.id, "Could not create environment."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
csp_environment_id = self._id()
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
|
||||||
BaselineProvisionException(
|
|
||||||
csp_environment_id, "Could not create environment baseline."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
|
||||||
|
|
||||||
return csp_environment_id
|
|
||||||
|
|
||||||
def create_atat_admin_user(self, auth_credentials, csp_environment_id):
|
|
||||||
self._authorize(auth_credentials)
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
|
||||||
UserProvisioningException(
|
|
||||||
csp_environment_id, "atat_admin", "Could not create admin user."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
|
||||||
|
|
||||||
return {"id": self._id(), "credentials": self._auth_credentials}
|
|
||||||
|
|
||||||
def create_tenant(self, payload):
|
|
||||||
"""
|
|
||||||
payload is an instance of TenantCSPPayload data class
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._authorize(payload.creds)
|
|
||||||
|
|
||||||
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)
|
|
||||||
# return tenant id, tenant owner id and tenant owner object id from:
|
|
||||||
response = {"tenantId": "string", "userId": "string", "objectId": "string"}
|
|
||||||
return {
|
|
||||||
"tenant_id": response["tenantId"],
|
|
||||||
"user_id": response["userId"],
|
|
||||||
"user_object_id": response["objectId"],
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
|
|
||||||
# call billing profile creation endpoint, specifying owner
|
|
||||||
# Payload:
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"displayName": "string",
|
|
||||||
"poNumber": "string",
|
|
||||||
"address": {
|
|
||||||
"firstName": "string",
|
|
||||||
"lastName": "string",
|
|
||||||
"companyName": "string",
|
|
||||||
"addressLine1": "string",
|
|
||||||
"addressLine2": "string",
|
|
||||||
"addressLine3": "string",
|
|
||||||
"city": "string",
|
|
||||||
"region": "string",
|
|
||||||
"country": "string",
|
|
||||||
"postalCode": "string"
|
|
||||||
},
|
|
||||||
"invoiceEmailOptIn": true,
|
|
||||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
|
||||||
"enabledAzurePlans": [
|
|
||||||
{
|
|
||||||
"skuId": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"clinBudget": {
|
|
||||||
"amount": 0,
|
|
||||||
"startDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"endDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"externalReferenceId": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# response will be mostly the same as the body, but we only really care about the id
|
|
||||||
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)
|
|
||||||
|
|
||||||
response = {"id": "string"}
|
|
||||||
return {"billing_profile_id": response["id"]}
|
|
||||||
|
|
||||||
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
|
|
||||||
self._authorize(auth_credentials)
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
|
||||||
UserProvisioningException(
|
|
||||||
user_info.environment.id,
|
|
||||||
user_info.application_role.user_id,
|
|
||||||
"Could not create user.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
|
||||||
return self._id()
|
|
||||||
|
|
||||||
def disable_user(self, auth_credentials, csp_user_id):
|
|
||||||
self._authorize(auth_credentials)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
|
||||||
UserRemovalException(csp_user_id, "Could not disable user."),
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._maybe(12)
|
|
||||||
|
|
||||||
def create_subscription(self, environment):
|
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_calculator_url(self):
|
|
||||||
return "https://www.rackspace.com/en-us/calculator"
|
|
||||||
|
|
||||||
def get_environment_login_url(self, environment):
|
|
||||||
"""Returns the login url for a given environment
|
|
||||||
"""
|
|
||||||
return "https://www.mycloud.com/my-env-login"
|
|
||||||
|
|
||||||
def _id(self):
|
|
||||||
return uuid4().hex
|
|
||||||
|
|
||||||
def _delay(self, min_secs, max_secs):
|
|
||||||
if self._with_delay:
|
|
||||||
duration = self._random.randrange(min_secs, max_secs)
|
|
||||||
self._sleep(duration)
|
|
||||||
|
|
||||||
def _maybe(self, pct):
|
|
||||||
return not self._with_failure or self._random.randrange(0, 100) < pct
|
|
||||||
|
|
||||||
def _maybe_raise(self, pct, exc):
|
|
||||||
if self._with_failure and self._maybe(pct):
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _auth_credentials(self):
|
|
||||||
return {"username": "mock-cloud", "pass": "shh"}
|
|
||||||
|
|
||||||
def _authorize(self, credentials):
|
|
||||||
self._delay(1, 5)
|
|
||||||
if self._with_authorization and credentials != self._auth_credentials:
|
|
||||||
raise self.AUTHENTICATION_EXCEPTION
|
|
||||||
|
|
||||||
|
|
||||||
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
|
|
||||||
AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI
|
|
||||||
SUBSCRIPTION_ID_REGEX = re.compile(
|
|
||||||
"subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})",
|
|
||||||
re.I,
|
|
||||||
)
|
|
||||||
|
|
||||||
# This needs to be a fully pathed role definition identifier, not just a UUID
|
|
||||||
REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000"
|
|
||||||
|
|
||||||
|
|
||||||
class AzureSDKProvider(object):
|
|
||||||
def __init__(self):
|
|
||||||
from azure.mgmt import subscription, authorization
|
|
||||||
import azure.graphrbac as graphrbac
|
|
||||||
import azure.common.credentials as credentials
|
|
||||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
|
||||||
|
|
||||||
self.subscription = subscription
|
|
||||||
self.authorization = authorization
|
|
||||||
self.graphrbac = graphrbac
|
|
||||||
self.credentials = credentials
|
|
||||||
# may change to a JEDI cloud
|
|
||||||
self.cloud = AZURE_PUBLIC_CLOUD
|
|
||||||
|
|
||||||
|
|
||||||
class AzureCloudProvider(CloudProviderInterface):
|
|
||||||
def __init__(self, config, azure_sdk_provider=None):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
self.client_id = config["AZURE_CLIENT_ID"]
|
|
||||||
self.secret_key = config["AZURE_SECRET_KEY"]
|
|
||||||
self.tenant_id = config["AZURE_TENANT_ID"]
|
|
||||||
|
|
||||||
if azure_sdk_provider is None:
|
|
||||||
self.sdk = AzureSDKProvider()
|
|
||||||
else:
|
|
||||||
self.sdk = azure_sdk_provider
|
|
||||||
|
|
||||||
def create_environment(
|
|
||||||
self, auth_credentials: Dict, user: User, environment: Environment
|
|
||||||
):
|
|
||||||
credentials = self._get_credential_obj(self._root_creds)
|
|
||||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
|
||||||
|
|
||||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
|
||||||
|
|
||||||
billing_profile_id = "?" # something chained from environment?
|
|
||||||
sku_id = AZURE_SKU_ID
|
|
||||||
# we want to set AT-AT as an owner here
|
|
||||||
# we could potentially associate subscriptions with "management groups" per DOD component
|
|
||||||
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
|
||||||
display_name,
|
|
||||||
billing_profile_id,
|
|
||||||
sku_id,
|
|
||||||
# owner=<AdPrincipal: for AT-AT user>
|
|
||||||
)
|
|
||||||
|
|
||||||
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
|
||||||
# TOs filed for the environment
|
|
||||||
billing_account_name = "?"
|
|
||||||
invoice_section_name = "?"
|
|
||||||
# We may also want to create billing sections in the enrollment account
|
|
||||||
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
|
||||||
billing_account_name, invoice_section_name, body
|
|
||||||
)
|
|
||||||
|
|
||||||
# the resulting object from this process is a link to the new subscription
|
|
||||||
# not a subscription model, so we'll have to unpack the ID
|
|
||||||
new_sub = sub_creation_operation.result()
|
|
||||||
|
|
||||||
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
|
||||||
if subscription_id:
|
|
||||||
return subscription_id
|
|
||||||
else:
|
|
||||||
# troublesome error, subscription should exist at this point
|
|
||||||
# but we just don't have a valid ID
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_atat_admin_user(
|
|
||||||
self, auth_credentials: Dict, csp_environment_id: str
|
|
||||||
) -> Dict:
|
|
||||||
root_creds = self._root_creds
|
|
||||||
credentials = self._get_credential_obj(root_creds)
|
|
||||||
|
|
||||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
|
||||||
subscription = sub_client.subscriptions.get(csp_environment_id)
|
|
||||||
|
|
||||||
managment_principal = self._get_management_service_principal()
|
|
||||||
|
|
||||||
auth_client = self.sdk.authorization.AuthorizationManagementClient(
|
|
||||||
credentials,
|
|
||||||
# TODO: Determine which subscription this needs to point at
|
|
||||||
# Once we're in a multi-sub environment
|
|
||||||
subscription.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create role assignment for
|
|
||||||
role_assignment_id = str(uuid4())
|
|
||||||
role_assignment_create_params = auth_client.role_assignments.models.RoleAssignmentCreateParameters(
|
|
||||||
role_definition_id=REMOTE_ROOT_ROLE_DEF_ID,
|
|
||||||
principal_id=managment_principal.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_client.role_assignments.create(
|
|
||||||
scope=f"/subscriptions/{subscription.id}/",
|
|
||||||
role_assignment_name=role_assignment_id,
|
|
||||||
parameters=role_assignment_create_params,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"csp_user_id": managment_principal.object_id,
|
|
||||||
"credentials": managment_principal.password_credentials,
|
|
||||||
"role_name": role_assignment_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_tenant(self, payload):
|
|
||||||
# auth as SP that is allowed to create tenant? (tenant creation sp creds)
|
|
||||||
# create tenant with owner details (populated from portfolio point of contact, pw is generated)
|
|
||||||
|
|
||||||
# return tenant id, tenant owner id and tenant owner object id from:
|
|
||||||
response = {"tenantId": "string", "userId": "string", "objectId": "string"}
|
|
||||||
return self._ok(
|
|
||||||
{
|
|
||||||
"tenant_id": response["tenantId"],
|
|
||||||
"user_id": response["userId"],
|
|
||||||
"user_object_id": response["objectId"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_billing_owner(self, creds, tenant_admin_details):
|
|
||||||
# authenticate as tenant_admin
|
|
||||||
# create billing owner identity
|
|
||||||
|
|
||||||
# TODO: Lookup response format
|
|
||||||
# Managed service identity?
|
|
||||||
response = {"id": "string"}
|
|
||||||
return self._ok({"billing_owner_id": response["id"]})
|
|
||||||
|
|
||||||
def assign_billing_owner(self, creds, billing_owner_id, tenant_id):
|
|
||||||
# TODO: Do we source role definition ID from config, api or self-defined?
|
|
||||||
# TODO: If from api,
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"principalId": "string",
|
|
||||||
"principalTenantId": "string",
|
|
||||||
"billingRoleDefinitionId": "string"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.ok()
|
|
||||||
|
|
||||||
def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
|
|
||||||
# call billing profile creation endpoint, specifying owner
|
|
||||||
# Payload:
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"displayName": "string",
|
|
||||||
"poNumber": "string",
|
|
||||||
"address": {
|
|
||||||
"firstName": "string",
|
|
||||||
"lastName": "string",
|
|
||||||
"companyName": "string",
|
|
||||||
"addressLine1": "string",
|
|
||||||
"addressLine2": "string",
|
|
||||||
"addressLine3": "string",
|
|
||||||
"city": "string",
|
|
||||||
"region": "string",
|
|
||||||
"country": "string",
|
|
||||||
"postalCode": "string"
|
|
||||||
},
|
|
||||||
"invoiceEmailOptIn": true,
|
|
||||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
|
||||||
"enabledAzurePlans": [
|
|
||||||
{
|
|
||||||
"skuId": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"clinBudget": {
|
|
||||||
"amount": 0,
|
|
||||||
"startDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"endDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"externalReferenceId": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# response will be mostly the same as the body, but we only really care about the id
|
|
||||||
response = {"id": "string"}
|
|
||||||
return self._ok({"billing_profile_id": response["id"]})
|
|
||||||
|
|
||||||
def report_clin(self, creds, clin_id, clin_amount, clin_start, clin_end, clin_to):
|
|
||||||
# should consumer be responsible for reporting each clin or
|
|
||||||
# should this take a list and manage the sequential reporting?
|
|
||||||
""" Payload
|
|
||||||
{
|
|
||||||
"enabledAzurePlans": [
|
|
||||||
{
|
|
||||||
"skuId": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"clinBudget": {
|
|
||||||
"amount": 0,
|
|
||||||
"startDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"endDate": "2019-12-18T16:47:40.909Z",
|
|
||||||
"externalReferenceId": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# we don't need any of the returned info for this
|
|
||||||
return self._ok()
|
|
||||||
|
|
||||||
def create_remote_admin(self, creds, tenant_details):
|
|
||||||
# create app/service principal within tenant, with name constructed from tenant details
|
|
||||||
# assign principal global admin
|
|
||||||
|
|
||||||
# needs to call out to CLI with tenant owner username/password, prototyping for that underway
|
|
||||||
|
|
||||||
# return identifier and creds to consumer for storage
|
|
||||||
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"}
|
|
||||||
return self._ok(
|
|
||||||
{
|
|
||||||
"client_id": response["clientId"],
|
|
||||||
"secret_key": response["secret_key"],
|
|
||||||
"tenant_id": response["tenantId"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
|
|
||||||
# use creds to update to force password recovery?
|
|
||||||
# not sure what the endpoint/method for this is, yet
|
|
||||||
|
|
||||||
return self._ok()
|
|
||||||
|
|
||||||
def create_billing_alerts(self, TBD):
|
|
||||||
# TODO: Add azure-mgmt-consumption for Budget and Notification entities/operations
|
|
||||||
# TODO: Determine how to auth against that API using the SDK, doesn't seeem possible at the moment
|
|
||||||
# TODO: billing alerts are registered as Notifications on Budget objects, which have start/end dates
|
|
||||||
# TODO: determine what the keys in the Notifications dict are supposed to be
|
|
||||||
# we may need to rotate budget objects when new TOs/CLINs are reported?
|
|
||||||
|
|
||||||
# we likely only want the budget ID, can be updated or replaced?
|
|
||||||
response = {"id": "id"}
|
|
||||||
return self._ok({"budget_id": response["id"]})
|
|
||||||
|
|
||||||
def _get_management_service_principal(self):
|
|
||||||
# we really should be using graph.microsoft.com, but i'm getting
|
|
||||||
# "expired token" errors for that
|
|
||||||
# graph_resource = "https://graph.microsoft.com"
|
|
||||||
graph_resource = "https://graph.windows.net"
|
|
||||||
graph_creds = self._get_credential_obj(
|
|
||||||
self._root_creds, resource=graph_resource
|
|
||||||
)
|
|
||||||
# I needed to set permissions for the graph.windows.net API before I
|
|
||||||
# could get this to work.
|
|
||||||
|
|
||||||
# how do we scope the graph client to the new subscription rather than
|
|
||||||
# the cloud0 subscription? tenant id seems to be separate from subscription id
|
|
||||||
graph_client = self.sdk.graphrbac.GraphRbacManagementClient(
|
|
||||||
graph_creds, self._root_creds.get("tenant_id")
|
|
||||||
)
|
|
||||||
|
|
||||||
# do we need to create a new application to manage each subscripition
|
|
||||||
# or should we manage access to each subscription from a single service
|
|
||||||
# principal with multiple role assignments?
|
|
||||||
app_display_name = "?" # name should reflect the subscription it exists
|
|
||||||
app_create_param = self.sdk.graphrbac.models.ApplicationCreateParameters(
|
|
||||||
display_name=app_display_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# we need the appropriate perms here:
|
|
||||||
# https://docs.microsoft.com/en-us/graph/api/application-post-applications?view=graph-rest-beta&tabs=http
|
|
||||||
# https://docs.microsoft.com/en-us/graph/permissions-reference#microsoft-graph-permission-names
|
|
||||||
# set app perms in app registration portal
|
|
||||||
# https://docs.microsoft.com/en-us/graph/auth-v2-service#2-configure-permissions-for-microsoft-graph
|
|
||||||
app: self.sdk.graphrbac.models.Application = graph_client.applications.create(
|
|
||||||
app_create_param
|
|
||||||
)
|
|
||||||
|
|
||||||
# create a new service principle for the new application, which should be scoped
|
|
||||||
# to the new subscription
|
|
||||||
app_id = app.app_id
|
|
||||||
sp_create_params = self.sdk.graphrbac.models.ServicePrincipalCreateParameters(
|
|
||||||
app_id=app_id, account_enabled=True
|
|
||||||
)
|
|
||||||
|
|
||||||
service_principal = graph_client.service_principals.create(sp_create_params)
|
|
||||||
|
|
||||||
return service_principal
|
|
||||||
|
|
||||||
def _extract_subscription_id(self, subscription_url):
|
|
||||||
sub_id_match = SUBSCRIPTION_ID_REGEX.match(subscription_url)
|
|
||||||
|
|
||||||
if sub_id_match:
|
|
||||||
return sub_id_match.group(1)
|
|
||||||
|
|
||||||
def _get_credential_obj(self, creds, resource=None):
|
|
||||||
|
|
||||||
return self.sdk.credentials.ServicePrincipalCredentials(
|
|
||||||
client_id=creds.get("client_id"),
|
|
||||||
secret=creds.get("secret_key"),
|
|
||||||
tenant=creds.get("tenant_id"),
|
|
||||||
resource=resource,
|
|
||||||
cloud_environment=self.sdk.cloud,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _make_tenant_admin_cred_obj(self, username, password):
|
|
||||||
return self.sdk.credentials.UserPassCredentials(username, password)
|
|
||||||
|
|
||||||
def _ok(self, body=None):
|
|
||||||
return self._make_response("ok", body)
|
|
||||||
|
|
||||||
def _error(self, body=None):
|
|
||||||
return self._make_response("error", body)
|
|
||||||
|
|
||||||
def _make_response(self, status, body=dict()):
|
|
||||||
"""Create body for responses from API
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
status {string} -- "ok" or "error"
|
|
||||||
body {dict} -- dict containing details of response or error, if applicable
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict -- status of call with body containing details
|
|
||||||
"""
|
|
||||||
return {"status": status, "body": body}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _root_creds(self):
|
|
||||||
return {
|
|
||||||
"client_id": self.client_id,
|
|
||||||
"secret_key": self.secret_key,
|
|
||||||
"tenant_id": self.tenant_id,
|
|
||||||
}
|
|
3
atst/domain/csp/cloud/__init__.py
Normal file
3
atst/domain/csp/cloud/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .azure_cloud_provider import AzureCloudProvider
|
||||||
|
from .cloud_provider_interface import CloudProviderInterface
|
||||||
|
from .mock_cloud_provider import MockCloudProvider
|
628
atst/domain/csp/cloud/azure_cloud_provider.py
Normal file
628
atst/domain/csp/cloud/azure_cloud_provider.py
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
import re
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
from typing import Dict
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from atst.models.application import Application
|
||||||
|
from atst.models.environment import Environment
|
||||||
|
from atst.models.user import User
|
||||||
|
|
||||||
|
from .cloud_provider_interface import CloudProviderInterface
|
||||||
|
from .exceptions import AuthenticationException
|
||||||
|
from .models import (
|
||||||
|
BillingInstructionCSPPayload,
|
||||||
|
BillingInstructionCSPResult,
|
||||||
|
BillingProfileCreationCSPPayload,
|
||||||
|
BillingProfileCreationCSPResult,
|
||||||
|
BillingProfileTenantAccessCSPPayload,
|
||||||
|
BillingProfileTenantAccessCSPResult,
|
||||||
|
BillingProfileVerificationCSPPayload,
|
||||||
|
BillingProfileVerificationCSPResult,
|
||||||
|
TaskOrderBillingCreationCSPPayload,
|
||||||
|
TaskOrderBillingCreationCSPResult,
|
||||||
|
TaskOrderBillingVerificationCSPPayload,
|
||||||
|
TaskOrderBillingVerificationCSPResult,
|
||||||
|
TenantCSPPayload,
|
||||||
|
TenantCSPResult,
|
||||||
|
)
|
||||||
|
from .policy import AzurePolicyManager
|
||||||
|
|
||||||
|
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
|
||||||
|
AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI
|
||||||
|
SUBSCRIPTION_ID_REGEX = re.compile(
|
||||||
|
"subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
|
||||||
|
# This needs to be a fully pathed role definition identifier, not just a UUID
|
||||||
|
REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000"
|
||||||
|
AZURE_MANAGEMENT_API = "https://management.azure.com"
|
||||||
|
|
||||||
|
|
||||||
|
class AzureSDKProvider(object):
|
||||||
|
def __init__(self):
|
||||||
|
from azure.mgmt import subscription, authorization, managementgroups
|
||||||
|
from azure.mgmt.resource import policy
|
||||||
|
import azure.graphrbac as graphrbac
|
||||||
|
import azure.common.credentials as credentials
|
||||||
|
import azure.identity as identity
|
||||||
|
from azure.keyvault import secrets
|
||||||
|
|
||||||
|
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||||
|
import adal
|
||||||
|
import requests
|
||||||
|
|
||||||
|
self.subscription = subscription
|
||||||
|
self.policy = policy
|
||||||
|
self.managementgroups = managementgroups
|
||||||
|
self.authorization = authorization
|
||||||
|
self.adal = adal
|
||||||
|
self.graphrbac = graphrbac
|
||||||
|
self.credentials = credentials
|
||||||
|
self.identity = identity
|
||||||
|
self.exceptions = exceptions
|
||||||
|
self.secrets = secrets
|
||||||
|
self.requests = requests
|
||||||
|
# may change to a JEDI cloud
|
||||||
|
self.cloud = AZURE_PUBLIC_CLOUD
|
||||||
|
|
||||||
|
|
||||||
|
class AzureCloudProvider(CloudProviderInterface):
|
||||||
|
def __init__(self, config, azure_sdk_provider=None):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.client_id = config["AZURE_CLIENT_ID"]
|
||||||
|
self.secret_key = config["AZURE_SECRET_KEY"]
|
||||||
|
self.tenant_id = config["AZURE_TENANT_ID"]
|
||||||
|
self.vault_url = config["AZURE_VAULT_URL"]
|
||||||
|
|
||||||
|
if azure_sdk_provider is None:
|
||||||
|
self.sdk = AzureSDKProvider()
|
||||||
|
else:
|
||||||
|
self.sdk = azure_sdk_provider
|
||||||
|
|
||||||
|
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||||
|
|
||||||
|
def set_secret(self, secret_key, secret_value):
|
||||||
|
credential = self._get_client_secret_credential_obj({})
|
||||||
|
secret_client = self.secrets.SecretClient(
|
||||||
|
vault_url=self.vault_url, credential=credential,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return secret_client.set_secret(secret_key, secret_value)
|
||||||
|
except self.exceptions.HttpResponseError:
|
||||||
|
app.logger.error(
|
||||||
|
f"Could not SET secret in Azure keyvault for key {secret_key}.",
|
||||||
|
exc_info=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_secret(self, secret_key):
|
||||||
|
credential = self._get_client_secret_credential_obj({})
|
||||||
|
secret_client = self.secrets.SecretClient(
|
||||||
|
vault_url=self.vault_url, credential=credential,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return secret_client.get_secret(secret_key).value
|
||||||
|
except self.exceptions.HttpResponseError:
|
||||||
|
app.logger.error(
|
||||||
|
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
||||||
|
exc_info=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_environment(
|
||||||
|
self, auth_credentials: Dict, user: User, environment: Environment
|
||||||
|
):
|
||||||
|
# since this operation would only occur within a tenant, should we source the tenant
|
||||||
|
# via lookup from environment once we've created the portfolio csp data schema
|
||||||
|
# something like this:
|
||||||
|
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
|
||||||
|
# though we'd probably source the whole credentials for these calls from the portfolio csp
|
||||||
|
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
|
||||||
|
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
|
||||||
|
credentials = self._get_credential_obj(self._root_creds)
|
||||||
|
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
||||||
|
management_group_id = "?" # management group id chained from environment
|
||||||
|
parent_id = "?" # from environment.application
|
||||||
|
|
||||||
|
management_group = self._create_management_group(
|
||||||
|
credentials, management_group_id, display_name, parent_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return management_group
|
||||||
|
|
||||||
|
def create_atat_admin_user(
|
||||||
|
self, auth_credentials: Dict, csp_environment_id: str
|
||||||
|
) -> Dict:
|
||||||
|
root_creds = self._root_creds
|
||||||
|
credentials = self._get_credential_obj(root_creds)
|
||||||
|
|
||||||
|
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||||
|
subscription = sub_client.subscriptions.get(csp_environment_id)
|
||||||
|
|
||||||
|
managment_principal = self._get_management_service_principal()
|
||||||
|
|
||||||
|
auth_client = self.sdk.authorization.AuthorizationManagementClient(
|
||||||
|
credentials,
|
||||||
|
# TODO: Determine which subscription this needs to point at
|
||||||
|
# Once we're in a multi-sub environment
|
||||||
|
subscription.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create role assignment for
|
||||||
|
role_assignment_id = str(uuid4())
|
||||||
|
role_assignment_create_params = auth_client.role_assignments.models.RoleAssignmentCreateParameters(
|
||||||
|
role_definition_id=REMOTE_ROOT_ROLE_DEF_ID,
|
||||||
|
principal_id=managment_principal.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_client.role_assignments.create(
|
||||||
|
scope=f"/subscriptions/{subscription.id}/",
|
||||||
|
role_assignment_name=role_assignment_id,
|
||||||
|
parameters=role_assignment_create_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"csp_user_id": managment_principal.object_id,
|
||||||
|
"credentials": managment_principal.password_credentials,
|
||||||
|
"role_name": role_assignment_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_application(self, auth_credentials: Dict, application: Application):
|
||||||
|
management_group_name = str(uuid4()) # can be anything, not just uuid
|
||||||
|
display_name = application.name # Does this need to be unique?
|
||||||
|
credentials = self._get_credential_obj(auth_credentials)
|
||||||
|
parent_id = "?" # application.portfolio.csp_details.management_group_id
|
||||||
|
|
||||||
|
return self._create_management_group(
|
||||||
|
credentials, management_group_name, display_name, parent_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_management_group(
|
||||||
|
self, credentials, management_group_id, display_name, parent_id=None,
|
||||||
|
):
|
||||||
|
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
|
||||||
|
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
|
||||||
|
id=parent_id
|
||||||
|
)
|
||||||
|
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
|
||||||
|
parent=create_parent_grp_info
|
||||||
|
)
|
||||||
|
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
|
||||||
|
name=management_group_id,
|
||||||
|
display_name=display_name,
|
||||||
|
details=create_mgmt_grp_details,
|
||||||
|
)
|
||||||
|
create_request = mgmgt_group_client.management_groups.create_or_update(
|
||||||
|
management_group_id, mgmt_grp_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
|
||||||
|
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
|
||||||
|
return create_request.result()
|
||||||
|
|
||||||
|
def _create_subscription(
|
||||||
|
self,
|
||||||
|
credentials,
|
||||||
|
display_name,
|
||||||
|
billing_profile_id,
|
||||||
|
sku_id,
|
||||||
|
management_group_id,
|
||||||
|
billing_account_name,
|
||||||
|
invoice_section_name,
|
||||||
|
):
|
||||||
|
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||||
|
|
||||||
|
billing_profile_id = "?" # where do we source this?
|
||||||
|
sku_id = AZURE_SKU_ID
|
||||||
|
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
||||||
|
# TOs filed for the environment
|
||||||
|
billing_account_name = "?" # from TO?
|
||||||
|
invoice_section_name = "?" # from TO?
|
||||||
|
|
||||||
|
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
||||||
|
display_name=display_name,
|
||||||
|
billing_profile_id=billing_profile_id,
|
||||||
|
sku_id=sku_id,
|
||||||
|
management_group_id=management_group_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We may also want to create billing sections in the enrollment account
|
||||||
|
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
||||||
|
billing_account_name, invoice_section_name, body
|
||||||
|
)
|
||||||
|
|
||||||
|
# the resulting object from this process is a link to the new subscription
|
||||||
|
# not a subscription model, so we'll have to unpack the ID
|
||||||
|
new_sub = sub_creation_operation.result()
|
||||||
|
|
||||||
|
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
||||||
|
if subscription_id:
|
||||||
|
return subscription_id
|
||||||
|
else:
|
||||||
|
# troublesome error, subscription should exist at this point
|
||||||
|
# but we just don't have a valid ID
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _create_policy_definition(
|
||||||
|
self, credentials, subscription_id, management_group_id, properties,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Requires credentials that have AZURE_MANAGEMENT_API
|
||||||
|
specified as the resource. The Service Principal
|
||||||
|
specified in the credentials must have the "Resource
|
||||||
|
Policy Contributor" role assigned with a scope at least
|
||||||
|
as high as the management group specified by
|
||||||
|
management_group_id.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
credentials -- ServicePrincipalCredentials
|
||||||
|
subscription_id -- str, ID of the subscription (just the UUID, not the path)
|
||||||
|
management_group_id -- str, ID of the management group (just the UUID, not the path)
|
||||||
|
properties -- dictionary, the "properties" section of a valid Azure policy definition document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
azure.mgmt.resource.policy.[api version].models.PolicyDefinition: the PolicyDefinition object provided to Azure
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TBD
|
||||||
|
"""
|
||||||
|
# TODO: which subscription would this be?
|
||||||
|
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
|
||||||
|
|
||||||
|
definition = client.policy_definitions.models.PolicyDefinition(
|
||||||
|
policy_type=properties.get("policyType"),
|
||||||
|
mode=properties.get("mode"),
|
||||||
|
display_name=properties.get("displayName"),
|
||||||
|
description=properties.get("description"),
|
||||||
|
policy_rule=properties.get("policyRule"),
|
||||||
|
parameters=properties.get("parameters"),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = properties.get("displayName")
|
||||||
|
|
||||||
|
return client.policy_definitions.create_or_update_at_management_group(
|
||||||
|
policy_definition_name=name,
|
||||||
|
parameters=definition,
|
||||||
|
management_group_id=management_group_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_tenant(self, payload: TenantCSPPayload):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
if sp_token is None:
|
||||||
|
raise AuthenticationException("Could not resolve token for tenant creation")
|
||||||
|
payload.password = token_urlsafe(16)
|
||||||
|
create_tenant_body = payload.dict(by_alias=True)
|
||||||
|
|
||||||
|
create_tenant_headers = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.sdk.requests.post(
|
||||||
|
"https://management.azure.com/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview",
|
||||||
|
json=create_tenant_body,
|
||||||
|
headers=create_tenant_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status_code == 200:
|
||||||
|
return self._ok(
|
||||||
|
TenantCSPResult(
|
||||||
|
**result.json(),
|
||||||
|
tenant_admin_password=payload.password,
|
||||||
|
tenant_admin_username=payload.user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_billing_profile_creation(
|
||||||
|
self, payload: BillingProfileCreationCSPPayload
|
||||||
|
):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
if sp_token is None:
|
||||||
|
raise AuthenticationException(
|
||||||
|
"Could not resolve token for billing profile creation"
|
||||||
|
)
|
||||||
|
|
||||||
|
create_billing_account_body = payload.dict(by_alias=True)
|
||||||
|
|
||||||
|
create_billing_account_headers = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
billing_account_create_url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview"
|
||||||
|
|
||||||
|
result = self.sdk.requests.post(
|
||||||
|
billing_account_create_url,
|
||||||
|
json=create_billing_account_body,
|
||||||
|
headers=create_billing_account_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status_code == 202:
|
||||||
|
# 202 has location/retry after headers
|
||||||
|
return self._ok(BillingProfileCreationCSPResult(**result.headers))
|
||||||
|
elif result.status_code == 200:
|
||||||
|
# NB: Swagger docs imply call can sometimes resolve immediately
|
||||||
|
return self._ok(BillingProfileVerificationCSPResult(**result.json()))
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_billing_profile_verification(
|
||||||
|
self, payload: BillingProfileVerificationCSPPayload
|
||||||
|
):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
if sp_token is None:
|
||||||
|
raise AuthenticationException(
|
||||||
|
"Could not resolve token for billing profile validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_header = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.sdk.requests.get(
|
||||||
|
payload.billing_profile_verify_url, headers=auth_header
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status_code == 202:
|
||||||
|
# 202 has location/retry after headers
|
||||||
|
return self._ok(BillingProfileCreationCSPResult(**result.headers))
|
||||||
|
elif result.status_code == 200:
|
||||||
|
return self._ok(BillingProfileVerificationCSPResult(**result.json()))
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_billing_profile_tenant_access(
|
||||||
|
self, payload: BillingProfileTenantAccessCSPPayload
|
||||||
|
):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
request_body = {
|
||||||
|
"properties": {
|
||||||
|
"principalTenantId": payload.tenant_id, # from tenant creation
|
||||||
|
"principalId": payload.user_object_id, # from tenant creationn
|
||||||
|
"roleDefinitionId": f"/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000000",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview"
|
||||||
|
|
||||||
|
result = self.sdk.requests.post(url, headers=headers, json=request_body)
|
||||||
|
if result.status_code == 201:
|
||||||
|
return self._ok(BillingProfileTenantAccessCSPResult(**result.json()))
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_task_order_billing_creation(
|
||||||
|
self, payload: TaskOrderBillingCreationCSPPayload
|
||||||
|
):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
request_body = [
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/enabledAzurePlans",
|
||||||
|
"value": [{"skuId": "0001"}],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
request_headers = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview"
|
||||||
|
|
||||||
|
result = self.sdk.requests.patch(
|
||||||
|
url, headers=request_headers, json=request_body
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status_code == 202:
|
||||||
|
# 202 has location/retry after headers
|
||||||
|
return self._ok(TaskOrderBillingCreationCSPResult(**result.headers))
|
||||||
|
elif result.status_code == 200:
|
||||||
|
return self._ok(TaskOrderBillingVerificationCSPResult(**result.json()))
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_task_order_billing_verification(
|
||||||
|
self, payload: TaskOrderBillingVerificationCSPPayload
|
||||||
|
):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
if sp_token is None:
|
||||||
|
raise AuthenticationException(
|
||||||
|
"Could not resolve token for task order billing validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_header = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.sdk.requests.get(
|
||||||
|
payload.task_order_billing_verify_url, headers=auth_header
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.status_code == 202:
|
||||||
|
# 202 has location/retry after headers
|
||||||
|
return self._ok(TaskOrderBillingCreationCSPResult(**result.headers))
|
||||||
|
elif result.status_code == 200:
|
||||||
|
return self._ok(TaskOrderBillingVerificationCSPResult(**result.json()))
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_billing_instruction(self, payload: BillingInstructionCSPPayload):
|
||||||
|
sp_token = self._get_sp_token(payload.creds)
|
||||||
|
if sp_token is None:
|
||||||
|
raise AuthenticationException(
|
||||||
|
"Could not resolve token for task order billing validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
"properties": {
|
||||||
|
"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.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview"
|
||||||
|
|
||||||
|
auth_header = {
|
||||||
|
"Authorization": f"Bearer {sp_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.sdk.requests.put(url, headers=auth_header, json=request_body)
|
||||||
|
|
||||||
|
if result.status_code == 200:
|
||||||
|
return self._ok(BillingInstructionCSPResult(**result.json()))
|
||||||
|
else:
|
||||||
|
return self._error(result.json())
|
||||||
|
|
||||||
|
def create_remote_admin(self, creds, tenant_details):
|
||||||
|
# create app/service principal within tenant, with name constructed from tenant details
|
||||||
|
# assign principal global admin
|
||||||
|
|
||||||
|
# needs to call out to CLI with tenant owner username/password, prototyping for that underway
|
||||||
|
|
||||||
|
# return identifier and creds to consumer for storage
|
||||||
|
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"}
|
||||||
|
return self._ok(
|
||||||
|
{
|
||||||
|
"client_id": response["clientId"],
|
||||||
|
"secret_key": response["secret_key"],
|
||||||
|
"tenant_id": response["tenantId"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
|
||||||
|
# use creds to update to force password recovery?
|
||||||
|
# not sure what the endpoint/method for this is, yet
|
||||||
|
|
||||||
|
return self._ok()
|
||||||
|
|
||||||
|
def create_billing_alerts(self, TBD):
|
||||||
|
# TODO: Add azure-mgmt-consumption for Budget and Notification entities/operations
|
||||||
|
# TODO: Determine how to auth against that API using the SDK, doesn't seeem possible at the moment
|
||||||
|
# TODO: billing alerts are registered as Notifications on Budget objects, which have start/end dates
|
||||||
|
# TODO: determine what the keys in the Notifications dict are supposed to be
|
||||||
|
# we may need to rotate budget objects when new TOs/CLINs are reported?
|
||||||
|
|
||||||
|
# we likely only want the budget ID, can be updated or replaced?
|
||||||
|
response = {"id": "id"}
|
||||||
|
return self._ok({"budget_id": response["id"]})
|
||||||
|
|
||||||
|
def _get_management_service_principal(self):
|
||||||
|
# we really should be using graph.microsoft.com, but i'm getting
|
||||||
|
# "expired token" errors for that
|
||||||
|
# graph_resource = "https://graph.microsoft.com"
|
||||||
|
graph_resource = "https://graph.windows.net"
|
||||||
|
graph_creds = self._get_credential_obj(
|
||||||
|
self._root_creds, resource=graph_resource
|
||||||
|
)
|
||||||
|
# I needed to set permissions for the graph.windows.net API before I
|
||||||
|
# could get this to work.
|
||||||
|
|
||||||
|
# how do we scope the graph client to the new subscription rather than
|
||||||
|
# the cloud0 subscription? tenant id seems to be separate from subscription id
|
||||||
|
graph_client = self.sdk.graphrbac.GraphRbacManagementClient(
|
||||||
|
graph_creds, self._root_creds.get("tenant_id")
|
||||||
|
)
|
||||||
|
|
||||||
|
# do we need to create a new application to manage each subscripition
|
||||||
|
# or should we manage access to each subscription from a single service
|
||||||
|
# principal with multiple role assignments?
|
||||||
|
app_display_name = "?" # name should reflect the subscription it exists
|
||||||
|
app_create_param = self.sdk.graphrbac.models.ApplicationCreateParameters(
|
||||||
|
display_name=app_display_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# we need the appropriate perms here:
|
||||||
|
# https://docs.microsoft.com/en-us/graph/api/application-post-applications?view=graph-rest-beta&tabs=http
|
||||||
|
# https://docs.microsoft.com/en-us/graph/permissions-reference#microsoft-graph-permission-names
|
||||||
|
# set app perms in app registration portal
|
||||||
|
# https://docs.microsoft.com/en-us/graph/auth-v2-service#2-configure-permissions-for-microsoft-graph
|
||||||
|
app: self.sdk.graphrbac.models.Application = graph_client.applications.create(
|
||||||
|
app_create_param
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a new service principle for the new application, which should be scoped
|
||||||
|
# to the new subscription
|
||||||
|
app_id = app.app_id
|
||||||
|
sp_create_params = self.sdk.graphrbac.models.ServicePrincipalCreateParameters(
|
||||||
|
app_id=app_id, account_enabled=True
|
||||||
|
)
|
||||||
|
|
||||||
|
service_principal = graph_client.service_principals.create(sp_create_params)
|
||||||
|
|
||||||
|
return service_principal
|
||||||
|
|
||||||
|
def _extract_subscription_id(self, subscription_url):
|
||||||
|
sub_id_match = SUBSCRIPTION_ID_REGEX.match(subscription_url)
|
||||||
|
|
||||||
|
if sub_id_match:
|
||||||
|
return sub_id_match.group(1)
|
||||||
|
|
||||||
|
def _get_sp_token(self, creds):
|
||||||
|
home_tenant_id = creds.get("home_tenant_id")
|
||||||
|
client_id = creds.get("client_id")
|
||||||
|
secret_key = creds.get("secret_key")
|
||||||
|
|
||||||
|
# TODO: Make endpoints consts or configs
|
||||||
|
authentication_endpoint = "https://login.microsoftonline.com/"
|
||||||
|
resource = "https://management.azure.com/"
|
||||||
|
|
||||||
|
context = self.sdk.adal.AuthenticationContext(
|
||||||
|
authentication_endpoint + home_tenant_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: handle failure states here
|
||||||
|
token_response = context.acquire_token_with_client_credentials(
|
||||||
|
resource, client_id, secret_key
|
||||||
|
)
|
||||||
|
|
||||||
|
return token_response.get("accessToken", None)
|
||||||
|
|
||||||
|
def _get_credential_obj(self, creds, resource=None):
|
||||||
|
return self.sdk.credentials.ServicePrincipalCredentials(
|
||||||
|
client_id=creds.get("client_id"),
|
||||||
|
secret=creds.get("secret_key"),
|
||||||
|
tenant=creds.get("tenant_id"),
|
||||||
|
resource=resource,
|
||||||
|
cloud_environment=self.sdk.cloud,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_client_secret_credential_obj(self, creds):
|
||||||
|
return self.sdk.identity.ClientSecretCredential(
|
||||||
|
tenant_id=creds.get("tenant_id"),
|
||||||
|
client_id=creds.get("client_id"),
|
||||||
|
client_secret=creds.get("secret_key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_tenant_admin_cred_obj(self, username, password):
|
||||||
|
return self.sdk.credentials.UserPassCredentials(username, password)
|
||||||
|
|
||||||
|
def _ok(self, body=None):
|
||||||
|
return self._make_response("ok", body)
|
||||||
|
|
||||||
|
def _error(self, body=None):
|
||||||
|
return self._make_response("error", body)
|
||||||
|
|
||||||
|
def _make_response(self, status, body=dict()):
|
||||||
|
"""Create body for responses from API
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
status {string} -- "ok" or "error"
|
||||||
|
body {dict} -- dict containing details of response or error, if applicable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict -- status of call with body containing details
|
||||||
|
"""
|
||||||
|
return {"status": status, "body": body}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _root_creds(self):
|
||||||
|
return {
|
||||||
|
"client_id": self.client_id,
|
||||||
|
"secret_key": self.secret_key,
|
||||||
|
"tenant_id": self.tenant_id,
|
||||||
|
}
|
126
atst/domain/csp/cloud/cloud_provider_interface.py
Normal file
126
atst/domain/csp/cloud/cloud_provider_interface.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from atst.models.user import User
|
||||||
|
from atst.models.environment import Environment
|
||||||
|
from atst.models.environment_role import EnvironmentRole
|
||||||
|
|
||||||
|
|
||||||
|
class CloudProviderInterface:
|
||||||
|
def set_secret(self, secret_key: str, secret_value: str):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_secret(self, secret_key: str):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def root_creds(self) -> Dict:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def create_environment(
|
||||||
|
self, auth_credentials: Dict, user: User, environment: Environment
|
||||||
|
) -> str:
|
||||||
|
"""Create a new environment in the CSP.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
auth_credentials -- Object containing CSP account credentials
|
||||||
|
user -- ATAT user authorizing the environment creation
|
||||||
|
environment -- ATAT Environment model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: ID of created environment
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthenticationException: Problem with the credentials
|
||||||
|
AuthorizationException: Credentials not authorized for current action(s)
|
||||||
|
ConnectionException: Issue with the CSP API connection
|
||||||
|
UnknownServerException: Unknown issue on the CSP side
|
||||||
|
EnvironmentExistsException: Environment already exists and has been created
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def create_atat_admin_user(
|
||||||
|
self, auth_credentials: Dict, csp_environment_id: str
|
||||||
|
) -> Dict:
|
||||||
|
"""Creates a new, programmatic user in the CSP. Grants this user full permissions to administer
|
||||||
|
the CSP.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
auth_credentials -- Object containing CSP account credentials
|
||||||
|
csp_environment_id -- ID of the CSP Environment the admin user should be created in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
object: Object representing new remote admin user, including credentials
|
||||||
|
Something like:
|
||||||
|
{
|
||||||
|
"user_id": string,
|
||||||
|
"credentials": dict, # structure TBD based on csp
|
||||||
|
}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthenticationException: Problem with the credentials
|
||||||
|
AuthorizationException: Credentials not authorized for current action(s)
|
||||||
|
ConnectionException: Issue with the CSP API connection
|
||||||
|
UnknownServerException: Unknown issue on the CSP side
|
||||||
|
UserProvisioningException: Problem creating the root user
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def create_or_update_user(
|
||||||
|
self, auth_credentials: Dict, user_info: EnvironmentRole, csp_role_id: str
|
||||||
|
) -> str:
|
||||||
|
"""Creates a user or updates an existing user's role.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
auth_credentials -- Object containing CSP account credentials
|
||||||
|
user_info -- instance of EnvironmentRole containing user data
|
||||||
|
if it has a csp_user_id it will try to update that user
|
||||||
|
csp_role_id -- The id of the role the user should be given in the CSP
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: Returns the interal csp_user_id of the created/updated user account
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthenticationException: Problem with the credentials
|
||||||
|
AuthorizationException: Credentials not authorized for current action(s)
|
||||||
|
ConnectionException: Issue with the CSP API connection
|
||||||
|
UnknownServerException: Unknown issue on the CSP side
|
||||||
|
UserProvisioningException: User couldn't be created or modified
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def disable_user(self, auth_credentials: Dict, csp_user_id: str) -> bool:
|
||||||
|
"""Revoke all privileges for a user. Used to prevent user access while a full
|
||||||
|
delete is being processed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
auth_credentials -- Object containing CSP account credentials
|
||||||
|
csp_user_id -- CSP internal user identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- True on success
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthenticationException: Problem with the credentials
|
||||||
|
AuthorizationException: Credentials not authorized for current action(s)
|
||||||
|
ConnectionException: Issue with the CSP API connection
|
||||||
|
UnknownServerException: Unknown issue on the CSP side
|
||||||
|
UserRemovalException: User couldn't be suspended
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_calculator_url(self) -> str:
|
||||||
|
"""Returns the calculator url for the CSP.
|
||||||
|
This will likely be a static property elsewhere once a CSP is chosen.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_environment_login_url(self, environment) -> str:
|
||||||
|
"""Returns the login url for a given environment
|
||||||
|
This may move to be a computed property on the Environment domain object
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def create_subscription(self, environment):
|
||||||
|
"""Returns True if a new subscription has been created or raises an
|
||||||
|
exception if an error occurs while creating a subscription.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
131
atst/domain/csp/cloud/exceptions.py
Normal file
131
atst/domain/csp/cloud/exceptions.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
class GeneralCSPException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OperationInProgressException(GeneralCSPException):
|
||||||
|
"""Throw this for instances when the CSP reports that the current entity is already
|
||||||
|
being operated on/created/deleted/etc
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, operation_desc):
|
||||||
|
self.operation_desc = operation_desc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "An operation for this entity is already in progress: {}".format(
|
||||||
|
self.operation_desc
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationException(GeneralCSPException):
|
||||||
|
"""Throw this for instances when there is a problem with the auth credentials:
|
||||||
|
* Missing credentials
|
||||||
|
* Incorrect credentials
|
||||||
|
* Other credential problems
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_error):
|
||||||
|
self.auth_error = auth_error
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "An error occurred with authentication: {}".format(self.auth_error)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationException(GeneralCSPException):
|
||||||
|
"""Throw this for instances when the current credentials are not authorized
|
||||||
|
for the current action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_error):
|
||||||
|
self.auth_error = auth_error
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "An error occurred with authorization: {}".format(self.auth_error)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionException(GeneralCSPException):
|
||||||
|
"""A general problem with the connection, timeouts or unresolved endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, connection_error):
|
||||||
|
self.connection_error = connection_error
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "Could not connect to cloud provider: {}".format(self.connection_error)
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownServerException(GeneralCSPException):
|
||||||
|
"""An error occured on the CSP side (5xx) and we don't know why
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, server_error):
|
||||||
|
self.server_error = server_error
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "A server error occured: {}".format(self.server_error)
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentCreationException(GeneralCSPException):
|
||||||
|
"""If there was an error in creating the environment
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, env_identifier, reason):
|
||||||
|
self.env_identifier = env_identifier
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "The envionment {} couldn't be created: {}".format(
|
||||||
|
self.env_identifier, self.reason
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProvisioningException(GeneralCSPException):
|
||||||
|
"""Failed to provision a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, env_identifier, user_identifier, reason):
|
||||||
|
self.env_identifier = env_identifier
|
||||||
|
self.user_identifier = user_identifier
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "Failed to create user {} for environment {}: {}".format(
|
||||||
|
self.user_identifier, self.env_identifier, self.reason
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRemovalException(GeneralCSPException):
|
||||||
|
"""Failed to remove a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, user_csp_id, reason):
|
||||||
|
self.user_csp_id = user_csp_id
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "Failed to suspend or delete user {}: {}".format(
|
||||||
|
self.user_csp_id, self.reason
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaselineProvisionException(GeneralCSPException):
|
||||||
|
"""If there's any issues standing up whatever is required
|
||||||
|
for an environment baseline
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, env_identifier, reason):
|
||||||
|
self.env_identifier = env_identifier
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "Could not complete baseline provisioning for environment ({}): {}".format(
|
||||||
|
self.env_identifier, self.reason
|
||||||
|
)
|
342
atst/domain/csp/cloud/mock_cloud_provider.py
Normal file
342
atst/domain/csp/cloud/mock_cloud_provider.py
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from atst.domain.csp.cloud.exceptions import (
|
||||||
|
BaselineProvisionException,
|
||||||
|
EnvironmentCreationException,
|
||||||
|
GeneralCSPException,
|
||||||
|
UserProvisioningException,
|
||||||
|
UserRemovalException,
|
||||||
|
)
|
||||||
|
from atst.domain.csp.cloud.models import BillingProfileTenantAccessCSPResult
|
||||||
|
|
||||||
|
from .cloud_provider_interface import CloudProviderInterface
|
||||||
|
from .exceptions import (
|
||||||
|
AuthenticationException,
|
||||||
|
AuthorizationException,
|
||||||
|
ConnectionException,
|
||||||
|
UnknownServerException,
|
||||||
|
)
|
||||||
|
from .models import (
|
||||||
|
BillingInstructionCSPPayload,
|
||||||
|
BillingInstructionCSPResult,
|
||||||
|
BillingProfileCreationCSPPayload,
|
||||||
|
BillingProfileCreationCSPResult,
|
||||||
|
BillingProfileVerificationCSPPayload,
|
||||||
|
BillingProfileVerificationCSPResult,
|
||||||
|
TaskOrderBillingCreationCSPPayload,
|
||||||
|
TaskOrderBillingCreationCSPResult,
|
||||||
|
TaskOrderBillingVerificationCSPPayload,
|
||||||
|
TaskOrderBillingVerificationCSPResult,
|
||||||
|
TenantCSPPayload,
|
||||||
|
TenantCSPResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MockCloudProvider(CloudProviderInterface):
|
||||||
|
|
||||||
|
# TODO: All of these constants
|
||||||
|
AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.")
|
||||||
|
AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.")
|
||||||
|
NETWORK_EXCEPTION = ConnectionException("Network failure.")
|
||||||
|
SERVER_EXCEPTION = UnknownServerException("Not our fault.")
|
||||||
|
|
||||||
|
SERVER_FAILURE_PCT = 1
|
||||||
|
NETWORK_FAILURE_PCT = 7
|
||||||
|
ENV_CREATE_FAILURE_PCT = 12
|
||||||
|
ATAT_ADMIN_CREATE_FAILURE_PCT = 12
|
||||||
|
UNAUTHORIZED_RATE = 2
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config, with_delay=True, with_failure=True, with_authorization=True
|
||||||
|
):
|
||||||
|
from time import sleep
|
||||||
|
import random
|
||||||
|
|
||||||
|
self._with_delay = with_delay
|
||||||
|
self._with_failure = with_failure
|
||||||
|
self._with_authorization = with_authorization
|
||||||
|
self._sleep = sleep
|
||||||
|
self._random = random
|
||||||
|
|
||||||
|
def root_creds(self):
|
||||||
|
return self._auth_credentials
|
||||||
|
|
||||||
|
def set_secret(self, secret_key: str, secret_value: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_secret(self, secret_key: str, default=dict()):
|
||||||
|
return default
|
||||||
|
|
||||||
|
def create_environment(self, auth_credentials, user, environment):
|
||||||
|
self._authorize(auth_credentials)
|
||||||
|
|
||||||
|
self._delay(1, 5)
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
self._maybe_raise(
|
||||||
|
self.ENV_CREATE_FAILURE_PCT,
|
||||||
|
EnvironmentCreationException(
|
||||||
|
environment.id, "Could not create environment."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
csp_environment_id = self._id()
|
||||||
|
|
||||||
|
self._delay(1, 5)
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
self._maybe_raise(
|
||||||
|
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
||||||
|
BaselineProvisionException(
|
||||||
|
csp_environment_id, "Could not create environment baseline."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||||
|
|
||||||
|
return csp_environment_id
|
||||||
|
|
||||||
|
def create_atat_admin_user(self, auth_credentials, csp_environment_id):
|
||||||
|
self._authorize(auth_credentials)
|
||||||
|
|
||||||
|
self._delay(1, 5)
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
self._maybe_raise(
|
||||||
|
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
||||||
|
UserProvisioningException(
|
||||||
|
csp_environment_id, "atat_admin", "Could not create admin user."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||||
|
|
||||||
|
return {"id": self._id(), "credentials": self._auth_credentials}
|
||||||
|
|
||||||
|
def create_tenant(self, payload: TenantCSPPayload):
|
||||||
|
"""
|
||||||
|
payload is an instance of TenantCSPPayload data class
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._authorize(payload.creds)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return TenantCSPResult(
|
||||||
|
**{
|
||||||
|
"tenant_id": "",
|
||||||
|
"user_id": "",
|
||||||
|
"user_object_id": "",
|
||||||
|
"tenant_admin_username": "test",
|
||||||
|
"tenant_admin_password": "test",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_billing_profile_creation(
|
||||||
|
self, payload: BillingProfileCreationCSPPayload
|
||||||
|
):
|
||||||
|
# response will be mostly the same as the body, but we only really care about the id
|
||||||
|
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 BillingProfileCreationCSPResult(
|
||||||
|
**dict(
|
||||||
|
billing_profile_verify_url="https://zombo.com",
|
||||||
|
billing_profile_retry_after=10,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_billing_profile_verification(
|
||||||
|
self, payload: BillingProfileVerificationCSPPayload
|
||||||
|
):
|
||||||
|
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 BillingProfileVerificationCSPResult(
|
||||||
|
**{
|
||||||
|
"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": [],
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_billing_profile_tenant_access(self, payload):
|
||||||
|
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 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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
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": {
|
||||||
|
"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/billingAccounts/billingProfiles",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
|
||||||
|
self._authorize(auth_credentials)
|
||||||
|
|
||||||
|
self._delay(1, 5)
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
self._maybe_raise(
|
||||||
|
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
||||||
|
UserProvisioningException(
|
||||||
|
user_info.environment.id,
|
||||||
|
user_info.application_role.user_id,
|
||||||
|
"Could not create user.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||||
|
return self._id()
|
||||||
|
|
||||||
|
def disable_user(self, auth_credentials, csp_user_id):
|
||||||
|
self._authorize(auth_credentials)
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
|
||||||
|
self._maybe_raise(
|
||||||
|
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
||||||
|
UserRemovalException(csp_user_id, "Could not disable user."),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._maybe(12)
|
||||||
|
|
||||||
|
def create_subscription(self, environment):
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_calculator_url(self):
|
||||||
|
return "https://www.rackspace.com/en-us/calculator"
|
||||||
|
|
||||||
|
def get_environment_login_url(self, environment):
|
||||||
|
"""Returns the login url for a given environment
|
||||||
|
"""
|
||||||
|
return "https://www.mycloud.com/my-env-login"
|
||||||
|
|
||||||
|
def _id(self):
|
||||||
|
return uuid4().hex
|
||||||
|
|
||||||
|
def _delay(self, min_secs, max_secs):
|
||||||
|
if self._with_delay:
|
||||||
|
duration = self._random.randrange(min_secs, max_secs)
|
||||||
|
self._sleep(duration)
|
||||||
|
|
||||||
|
def _maybe(self, pct):
|
||||||
|
return not self._with_failure or self._random.randrange(0, 100) < pct
|
||||||
|
|
||||||
|
def _maybe_raise(self, pct, exc):
|
||||||
|
if self._with_failure and self._maybe(pct):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _auth_credentials(self):
|
||||||
|
return {"username": "mock-cloud", "password": "shh"} # pragma: allowlist secret
|
||||||
|
|
||||||
|
def _authorize(self, credentials):
|
||||||
|
self._delay(1, 5)
|
||||||
|
if self._with_authorization and credentials != self._auth_credentials:
|
||||||
|
raise self.AUTHENTICATION_EXCEPTION
|
234
atst/domain/csp/cloud/models.py
Normal file
234
atst/domain/csp/cloud/models.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
|
from atst.utils import snake_to_camel
|
||||||
|
|
||||||
|
|
||||||
|
class AliasModel(BaseModel):
|
||||||
|
"""
|
||||||
|
This provides automatic camel <-> snake conversion for serializing to/from json
|
||||||
|
You can override the alias generation in subclasses by providing a Config that defines
|
||||||
|
a fields property with a dict mapping variables to their cast names, for cases like:
|
||||||
|
* some_url:someURL
|
||||||
|
* user_object_id:objectId
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
alias_generator = snake_to_camel
|
||||||
|
allow_population_by_field_name = True
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCSPPayload(AliasModel):
|
||||||
|
# {"username": "mock-cloud", "pass": "shh"}
|
||||||
|
creds: Dict
|
||||||
|
|
||||||
|
def dict(self, *args, **kwargs):
|
||||||
|
exclude = {"creds"}
|
||||||
|
if "exclude" not in kwargs:
|
||||||
|
kwargs["exclude"] = exclude
|
||||||
|
else:
|
||||||
|
kwargs["exclude"].update(exclude)
|
||||||
|
|
||||||
|
return super().dict(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TenantCSPPayload(BaseCSPPayload):
|
||||||
|
user_id: str
|
||||||
|
password: Optional[str]
|
||||||
|
domain_name: str
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
country_code: str
|
||||||
|
password_recovery_email_address: str
|
||||||
|
|
||||||
|
|
||||||
|
class TenantCSPResult(AliasModel):
|
||||||
|
user_id: str
|
||||||
|
tenant_id: str
|
||||||
|
user_object_id: str
|
||||||
|
|
||||||
|
tenant_admin_username: Optional[str]
|
||||||
|
tenant_admin_password: Optional[str]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"user_object_id": "objectId",
|
||||||
|
}
|
||||||
|
|
||||||
|
def dict(self, *args, **kwargs):
|
||||||
|
exclude = {"tenant_admin_username", "tenant_admin_password"}
|
||||||
|
if "exclude" not in kwargs:
|
||||||
|
kwargs["exclude"] = exclude
|
||||||
|
else:
|
||||||
|
kwargs["exclude"].update(exclude)
|
||||||
|
|
||||||
|
return super().dict(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_creds(self):
|
||||||
|
return {
|
||||||
|
"tenant_admin_username": self.tenant_admin_username,
|
||||||
|
"tenant_admin_password": self.tenant_admin_password,
|
||||||
|
"tenant_id": self.tenant_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileAddress(AliasModel):
|
||||||
|
company_name: str
|
||||||
|
address_line_1: str
|
||||||
|
city: str
|
||||||
|
region: str
|
||||||
|
country: str
|
||||||
|
postal_code: str
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileCLINBudget(AliasModel):
|
||||||
|
clin_budget: Dict
|
||||||
|
"""
|
||||||
|
"clinBudget": {
|
||||||
|
"amount": 0,
|
||||||
|
"startDate": "2019-12-18T16:47:40.909Z",
|
||||||
|
"endDate": "2019-12-18T16:47:40.909Z",
|
||||||
|
"externalReferenceId": "string"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileCreationCSPPayload(BaseCSPPayload):
|
||||||
|
tenant_id: str
|
||||||
|
billing_profile_display_name: str
|
||||||
|
billing_account_name: str
|
||||||
|
enabled_azure_plans: Optional[List[str]]
|
||||||
|
address: BillingProfileAddress
|
||||||
|
|
||||||
|
@validator("enabled_azure_plans", pre=True, always=True)
|
||||||
|
def default_enabled_azure_plans(cls, v):
|
||||||
|
"""
|
||||||
|
Normally you'd implement this by setting the field with a value of:
|
||||||
|
dataclasses.field(default_factory=list)
|
||||||
|
but that prevents the object from being correctly pickled, so instead we need
|
||||||
|
to rely on a validator to ensure this has an empty value when not specified
|
||||||
|
"""
|
||||||
|
return v or []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {"billing_profile_display_name": "displayName"}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileCreationCSPResult(AliasModel):
|
||||||
|
billing_profile_verify_url: str
|
||||||
|
billing_profile_retry_after: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"billing_profile_verify_url": "Location",
|
||||||
|
"billing_profile_retry_after": "Retry-After",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileVerificationCSPPayload(BaseCSPPayload):
|
||||||
|
billing_profile_verify_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class BillingInvoiceSection(AliasModel):
|
||||||
|
invoice_section_id: str
|
||||||
|
invoice_section_name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {"invoice_section_id": "id", "invoice_section_name": "name"}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileProperties(AliasModel):
|
||||||
|
address: BillingProfileAddress
|
||||||
|
billing_profile_display_name: str
|
||||||
|
invoice_sections: List[BillingInvoiceSection]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {"billing_profile_display_name": "displayName"}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileVerificationCSPResult(AliasModel):
|
||||||
|
billing_profile_id: str
|
||||||
|
billing_profile_name: str
|
||||||
|
billing_profile_properties: BillingProfileProperties
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"billing_profile_id": "id",
|
||||||
|
"billing_profile_name": "name",
|
||||||
|
"billing_profile_properties": "properties",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileTenantAccessCSPPayload(BaseCSPPayload):
|
||||||
|
tenant_id: str
|
||||||
|
user_object_id: str
|
||||||
|
billing_account_name: str
|
||||||
|
billing_profile_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileTenantAccessCSPResult(AliasModel):
|
||||||
|
billing_role_assignment_id: str
|
||||||
|
billing_role_assignment_name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"billing_role_assignment_id": "id",
|
||||||
|
"billing_role_assignment_name": "name",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOrderBillingCreationCSPPayload(BaseCSPPayload):
|
||||||
|
billing_account_name: str
|
||||||
|
billing_profile_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOrderBillingCreationCSPResult(AliasModel):
|
||||||
|
task_order_billing_verify_url: str
|
||||||
|
task_order_retry_after: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"task_order_billing_verify_url": "Location",
|
||||||
|
"task_order_retry_after": "Retry-After",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOrderBillingVerificationCSPPayload(BaseCSPPayload):
|
||||||
|
task_order_billing_verify_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class BillingProfileEnabledPlanDetails(AliasModel):
|
||||||
|
enabled_azure_plans: List[Dict]
|
||||||
|
|
||||||
|
|
||||||
|
class TaskOrderBillingVerificationCSPResult(AliasModel):
|
||||||
|
billing_profile_id: str
|
||||||
|
billing_profile_name: str
|
||||||
|
billing_profile_enabled_plan_details: BillingProfileEnabledPlanDetails
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"billing_profile_id": "id",
|
||||||
|
"billing_profile_name": "name",
|
||||||
|
"billing_profile_enabled_plan_details": "properties",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BillingInstructionCSPPayload(BaseCSPPayload):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class BillingInstructionCSPResult(AliasModel):
|
||||||
|
reported_clin_name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {
|
||||||
|
"reported_clin_name": "name",
|
||||||
|
}
|
@ -9,10 +9,10 @@ from atst.models import (
|
|||||||
EnvironmentRole,
|
EnvironmentRole,
|
||||||
PortfolioJobFailure,
|
PortfolioJobFailure,
|
||||||
)
|
)
|
||||||
from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException
|
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||||
|
from atst.domain.csp.cloud import CloudProviderInterface
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
|
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.models.utils import claim_for_update
|
from atst.models.utils import claim_for_update
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from flask import current_app as app
|
||||||
|
|
||||||
|
|
||||||
class StageStates(Enum):
|
class StageStates(Enum):
|
||||||
CREATED = "created"
|
CREATED = "created"
|
||||||
@ -9,8 +11,12 @@ class StageStates(Enum):
|
|||||||
|
|
||||||
class AzureStages(Enum):
|
class AzureStages(Enum):
|
||||||
TENANT = "tenant"
|
TENANT = "tenant"
|
||||||
BILLING_PROFILE = "billing profile"
|
BILLING_PROFILE_CREATION = "billing profile creation"
|
||||||
ADMIN_SUBSCRIPTION = "admin subscription"
|
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):
|
def _build_csp_states(csp_stages):
|
||||||
@ -31,14 +37,14 @@ def _build_csp_states(csp_stages):
|
|||||||
|
|
||||||
FSMStates = Enum("FSMStates", _build_csp_states(AzureStages))
|
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):
|
def _build_transitions(csp_stages):
|
||||||
transitions = []
|
transitions = []
|
||||||
states = []
|
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 stage_i, csp_stage in enumerate(csp_stages):
|
||||||
for state in StageStates:
|
for state in StageStates:
|
||||||
states.append(
|
states.append(
|
||||||
@ -99,6 +105,22 @@ class FSMMixin:
|
|||||||
{"trigger": "fail", "source": "*", "dest": FSMStates.FAILED,},
|
{"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):
|
def prepare_init(self, event):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -125,13 +147,3 @@ class FSMMixin:
|
|||||||
|
|
||||||
def after_reset(self, event):
|
def after_reset(self, event):
|
||||||
pass
|
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)
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum
|
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum
|
||||||
from sqlalchemy.orm import relationship, reconstructor
|
from sqlalchemy.orm import relationship, reconstructor
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
@ -8,8 +10,7 @@ from transitions.extensions.states import add_state_features, Tags
|
|||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
from atst.domain.csp.cloud import ConnectionException, UnknownServerException
|
from atst.domain.csp.cloud.exceptions import ConnectionException, UnknownServerException
|
||||||
from atst.domain.csp import MockCSP, AzureCSP, get_stage_csp_class
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.types import Id
|
from atst.models.types import Id
|
||||||
from atst.models.base import Base
|
from atst.models.base import Base
|
||||||
@ -17,6 +18,25 @@ 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, _build_transitions
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_to_classname(stage):
|
||||||
|
return "".join(map(lambda word: word.capitalize(), stage.split("_")))
|
||||||
|
|
||||||
|
|
||||||
|
def get_stage_csp_class(stage, class_type):
|
||||||
|
"""
|
||||||
|
given a stage name and class_type return the class
|
||||||
|
class_type is either 'payload' or 'result'
|
||||||
|
|
||||||
|
"""
|
||||||
|
cls_name = f"{_stage_to_classname(stage)}CSP{class_type.capitalize()}"
|
||||||
|
try:
|
||||||
|
return getattr(
|
||||||
|
importlib.import_module("atst.domain.csp.cloud.models"), cls_name
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
print("could not import CSP Result class <%s>" % cls_name)
|
||||||
|
|
||||||
|
|
||||||
@add_state_features(Tags)
|
@add_state_features(Tags)
|
||||||
class StateMachineWithTags(Machine):
|
class StateMachineWithTags(Machine):
|
||||||
pass
|
pass
|
||||||
@ -50,6 +70,9 @@ class PortfolioStateMachine(
|
|||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<PortfolioStateMachine(state='{self.current_state.name}', portfolio='{self.portfolio.name}'"
|
||||||
|
|
||||||
@reconstructor
|
@reconstructor
|
||||||
def attach_machine(self):
|
def attach_machine(self):
|
||||||
"""
|
"""
|
||||||
@ -73,109 +96,115 @@ class PortfolioStateMachine(
|
|||||||
return getattr(FSMStates, self.state)
|
return getattr(FSMStates, self.state)
|
||||||
return self.state
|
return self.state
|
||||||
|
|
||||||
def trigger_next_transition(self):
|
def trigger_next_transition(self, **kwargs):
|
||||||
state_obj = self.machine.get_state(self.state)
|
state_obj = self.machine.get_state(self.state)
|
||||||
|
|
||||||
if state_obj.is_system:
|
if state_obj.is_system:
|
||||||
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
||||||
# call the first trigger availabe for these two system states
|
# call the first trigger availabe for these two system states
|
||||||
trigger_name = self.machine.get_triggers(self.current_state.name)[0]
|
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:
|
elif self.current_state == FSMStates.STARTED:
|
||||||
# get the first trigger that starts with 'create_'
|
# get the first trigger that starts with 'create_'
|
||||||
create_trigger = list(
|
create_trigger = next(
|
||||||
filter(
|
filter(
|
||||||
lambda trigger: trigger.startswith("create_"),
|
lambda trigger: trigger.startswith("create_"),
|
||||||
self.machine.get_triggers(FSMStates.STARTED.name),
|
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.fail_stage(stage)
|
||||||
self.trigger(create_trigger)
|
|
||||||
|
|
||||||
elif state_obj.is_IN_PROGRESS:
|
elif state_obj.is_CREATED:
|
||||||
pass
|
# 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):
|
def after_in_progress_callback(self, event):
|
||||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||||
if stage == "tenant":
|
|
||||||
payload = dict( # nosec
|
# Accumulate payload w/ creds
|
||||||
creds={"username": "mock-cloud", "pass": "shh"},
|
payload = event.kwargs.get("csp_data")
|
||||||
user_id="123",
|
payload["creds"] = event.kwargs.get("creds")
|
||||||
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"},)
|
|
||||||
|
|
||||||
payload_data_cls = get_stage_csp_class(stage, "payload")
|
payload_data_cls = get_stage_csp_class(stage, "payload")
|
||||||
if not payload_data_cls:
|
if not payload_data_cls:
|
||||||
|
app.logger.info(f"could not resolve payload data class for stage {stage}")
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
try:
|
try:
|
||||||
payload_data = payload_data_cls(**payload)
|
payload_data = payload_data_cls(**payload)
|
||||||
except PydanticValidationError as exc:
|
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())
|
print(exc.json())
|
||||||
|
app.logger.info(payload)
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
|
|
||||||
csp = event.kwargs.get("csp")
|
# TODO: Determine best place to do this, maybe @reconstructor
|
||||||
if csp is not None:
|
self.csp = app.csp.cloud
|
||||||
self.csp = AzureCSP(app).cloud
|
|
||||||
else:
|
|
||||||
self.csp = MockCSP(app).cloud
|
|
||||||
|
|
||||||
for attempt in range(5):
|
try:
|
||||||
try:
|
func_name = f"create_{stage}"
|
||||||
response = getattr(self.csp, "create_" + stage)(payload_data)
|
response = getattr(self.csp, func_name)(payload_data)
|
||||||
except (ConnectionException, UnknownServerException) as exc:
|
if self.portfolio.csp_data is None:
|
||||||
print("caught exception. retry", attempt)
|
self.portfolio.csp_data = {}
|
||||||
continue
|
self.portfolio.csp_data.update(response.dict())
|
||||||
else:
|
db.session.add(self.portfolio)
|
||||||
break
|
db.session.commit()
|
||||||
else:
|
|
||||||
# failed all attempts
|
if getattr(response, "get_creds", None) is not None:
|
||||||
|
new_creds = response.get_creds()
|
||||||
|
# TODO: one way salted hash of tenant_id to use as kv key name?
|
||||||
|
tenant_id = new_creds.get("tenant_id")
|
||||||
|
secret = self.csp.get_secret(tenant_id, new_creds)
|
||||||
|
secret.update(new_creds)
|
||||||
|
self.csp.set_secret(tenant_id, secret)
|
||||||
|
except PydanticValidationError as exc:
|
||||||
|
app.logger.error(
|
||||||
|
f"Failed to cast response to valid result class {self.__repr__()}:",
|
||||||
|
exc_info=1,
|
||||||
|
)
|
||||||
|
app.logger.info(exc.json())
|
||||||
|
print(exc.json())
|
||||||
|
app.logger.info(payload_data)
|
||||||
|
self.fail_stage(stage)
|
||||||
|
except (ConnectionException, UnknownServerException) as exc:
|
||||||
|
app.logger.error(
|
||||||
|
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
|
||||||
|
)
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
|
|
||||||
if self.portfolio.csp_data is None:
|
|
||||||
self.portfolio.csp_data = {}
|
|
||||||
self.portfolio.csp_data[stage + "_data"] = response
|
|
||||||
db.session.add(self.portfolio)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
self.finish_stage(stage)
|
self.finish_stage(stage)
|
||||||
|
|
||||||
def is_csp_data_valid(self, event):
|
def is_csp_data_valid(self, event):
|
||||||
# check portfolio csp details json field for fields
|
"""
|
||||||
|
This function guards advancing states from *_IN_PROGRESS to *_COMPLETED.
|
||||||
|
"""
|
||||||
if self.portfolio.csp_data is None or not isinstance(
|
if self.portfolio.csp_data is None or not isinstance(
|
||||||
self.portfolio.csp_data, dict
|
self.portfolio.csp_data, dict
|
||||||
):
|
):
|
||||||
return False
|
print("no csp data")
|
||||||
|
|
||||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
|
||||||
stage_data = self.portfolio.csp_data.get(stage + "_data")
|
|
||||||
cls = get_stage_csp_class(stage, "result")
|
|
||||||
if not cls:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
cls(**stage_data)
|
|
||||||
except PydanticValidationError as exc:
|
|
||||||
print(exc.json())
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# print('failed condition', self.portfolio.csp_data)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def application_id(self):
|
def application_id(self):
|
||||||
return None
|
return None
|
||||||
|
@ -13,7 +13,7 @@ from atst.domain.environments import Environments
|
|||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
from atst.domain.audit_log import AuditLog
|
from atst.domain.audit_log import AuditLog
|
||||||
from atst.domain.csp.cloud import GeneralCSPException
|
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||||
from atst.domain.common import Paginator
|
from atst.domain.common import Paginator
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.invitations import ApplicationInvitations
|
from atst.domain.invitations import ApplicationInvitations
|
||||||
|
@ -25,6 +25,11 @@ def camel_to_snake(camel_cased):
|
|||||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
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):
|
def pick(keys, dct):
|
||||||
_keys = set(keys)
|
_keys = set(keys)
|
||||||
return {k: v for (k, v) in dct.items() if k in _keys}
|
return {k: v for (k, v) in dct.items() if k in _keys}
|
||||||
|
@ -8,7 +8,7 @@ run_python_lint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_python_typecheck() {
|
run_python_typecheck() {
|
||||||
run_command "mypy --ignore-missing-imports --follow-imports=skip atst/domain/csp/cloud.py"
|
run_command "mypy --ignore-missing-imports --follow-imports=skip atst/domain/csp/cloud/__init__.py"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from tests.factories import ApplicationFactory, EnvironmentFactory
|
||||||
|
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
||||||
|
|
||||||
from atst.domain.csp.cloud import AzureCloudProvider
|
from atst.domain.csp.cloud import AzureCloudProvider
|
||||||
|
from atst.domain.csp.cloud.models import (
|
||||||
|
BillingInstructionCSPPayload,
|
||||||
|
BillingInstructionCSPResult,
|
||||||
|
BillingProfileCreationCSPPayload,
|
||||||
|
BillingProfileCreationCSPResult,
|
||||||
|
BillingProfileTenantAccessCSPPayload,
|
||||||
|
BillingProfileTenantAccessCSPResult,
|
||||||
|
BillingProfileVerificationCSPPayload,
|
||||||
|
BillingProfileVerificationCSPResult,
|
||||||
|
TaskOrderBillingCreationCSPPayload,
|
||||||
|
TaskOrderBillingCreationCSPResult,
|
||||||
|
TaskOrderBillingVerificationCSPPayload,
|
||||||
|
TaskOrderBillingVerificationCSPResult,
|
||||||
|
TenantCSPPayload,
|
||||||
|
TenantCSPResult,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.mock_azure import mock_azure, AUTH_CREDENTIALS
|
creds = {
|
||||||
from tests.factories import EnvironmentFactory, ApplicationFactory
|
"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"
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
|
||||||
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
||||||
environment = EnvironmentFactory.create()
|
environment = EnvironmentFactory.create()
|
||||||
|
|
||||||
@ -51,14 +64,12 @@ def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
|||||||
assert result == subscription_id
|
assert result == subscription_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
|
||||||
def mock_management_group_create(mock_azure, spec_dict):
|
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(
|
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock(
|
||||||
**spec_dict
|
**spec_dict
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
|
||||||
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||||
environment = EnvironmentFactory.create()
|
environment = EnvironmentFactory.create()
|
||||||
|
|
||||||
@ -71,7 +82,6 @@ def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
|||||||
assert result.id == "Test Id"
|
assert result.id == "Test Id"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
|
||||||
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||||
application = ApplicationFactory.create()
|
application = ApplicationFactory.create()
|
||||||
|
|
||||||
@ -82,7 +92,6 @@ def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
|||||||
assert result.id == "Test Id"
|
assert result.id == "Test Id"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
|
||||||
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
||||||
environment_id = str(uuid4())
|
environment_id = str(uuid4())
|
||||||
|
|
||||||
@ -97,7 +106,6 @@ def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
|||||||
assert result.get("csp_user_id") == csp_user_id
|
assert result.get("csp_user_id") == csp_user_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
|
||||||
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
||||||
subscription_id = str(uuid4())
|
subscription_id = str(uuid4())
|
||||||
management_group_id = str(uuid4())
|
management_group_id = str(uuid4())
|
||||||
@ -121,3 +129,287 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
|||||||
policy_definition_name=properties.get("displayName"),
|
policy_definition_name=properties.get("displayName"),
|
||||||
parameters=mock_policy_definition,
|
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", # pragma: allowlist secret
|
||||||
|
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,4 +1,4 @@
|
|||||||
from atst.domain.csp.policy import AzurePolicyManager, AzurePolicy
|
from atst.domain.csp.cloud.policy import AzurePolicyManager, AzurePolicy
|
||||||
|
|
||||||
|
|
||||||
def test_portfolio_definitions():
|
def test_portfolio_definitions():
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import re
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
PortfolioFactory,
|
|
||||||
PortfolioStateMachineFactory,
|
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.models.portfolio_state_machine import get_stage_csp_class
|
||||||
|
|
||||||
|
# TODO: Write failure case tests
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def portfolio():
|
def portfolio():
|
||||||
portfolio = PortfolioFactory.create()
|
# TODO: setup clin/to as active/funded/ready
|
||||||
|
portfolio = CLINFactory.create().task_order.portfolio
|
||||||
return portfolio
|
return portfolio
|
||||||
|
|
||||||
|
|
||||||
@ -19,18 +27,132 @@ def test_fsm_creation(portfolio):
|
|||||||
assert sm.portfolio
|
assert sm.portfolio
|
||||||
|
|
||||||
|
|
||||||
def test_fsm_transition_start(portfolio):
|
def test_state_machine_trigger_next_transition(portfolio):
|
||||||
sm = PortfolioStateMachineFactory.create(portfolio=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
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("atst.domain.csp.cloud.MockCloudProvider")
|
||||||
|
def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
|
||||||
|
mock_cloud_provider._authorize.return_value = None
|
||||||
|
mock_cloud_provider._maybe_raise.return_value = None
|
||||||
|
sm: PortfolioStateMachine = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||||
assert sm.portfolio
|
assert sm.portfolio
|
||||||
assert sm.state == FSMStates.UNSTARTED
|
assert sm.state == FSMStates.UNSTARTED
|
||||||
|
|
||||||
# next_state does not create the trigger callbacks !!!
|
|
||||||
# sm.next_state()
|
|
||||||
|
|
||||||
sm.init()
|
sm.init()
|
||||||
assert sm.state == FSMStates.STARTING
|
assert sm.state == FSMStates.STARTING
|
||||||
|
|
||||||
sm.start()
|
sm.start()
|
||||||
assert sm.state == FSMStates.STARTED
|
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"} # pragma: allowlist secret
|
||||||
|
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", # pragma: allowlist secret
|
||||||
|
"domain_name": domain_name,
|
||||||
|
"first_name": ppoc.first_name,
|
||||||
|
"last_name": ppoc.last_name,
|
||||||
|
"country_code": "US",
|
||||||
|
"password_recovery_email_address": ppoc.email,
|
||||||
|
"address": { # TODO: TBD if we're sourcing this from data or config
|
||||||
|
"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:
|
||||||
|
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_SECRET_KEY": "MOCK",
|
||||||
"AZURE_TENANT_ID": "MOCK",
|
"AZURE_TENANT_ID": "MOCK",
|
||||||
"AZURE_POLICY_LOCATION": "policies",
|
"AZURE_POLICY_LOCATION": "policies",
|
||||||
|
"AZURE_VAULT_URL": "http://vault",
|
||||||
}
|
}
|
||||||
|
|
||||||
AUTH_CREDENTIALS = {
|
AUTH_CREDENTIALS = {
|
||||||
@ -53,16 +54,38 @@ def mock_policy():
|
|||||||
return Mock(spec=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):
|
class MockAzureSDK(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||||
|
|
||||||
self.subscription = mock_subscription()
|
self.subscription = mock_subscription()
|
||||||
self.authorization = mock_authorization()
|
self.authorization = mock_authorization()
|
||||||
|
self.policy = mock_policy()
|
||||||
|
self.adal = mock_adal()
|
||||||
self.managementgroups = mock_managementgroups()
|
self.managementgroups = mock_managementgroups()
|
||||||
self.graphrbac = mock_graphrbac()
|
self.graphrbac = mock_graphrbac()
|
||||||
self.credentials = mock_credentials()
|
self.credentials = mock_credentials()
|
||||||
self.policy = mock_policy()
|
self.policy = mock_policy()
|
||||||
|
self.secrets = mock_secrets()
|
||||||
|
self.requests = mock_requests()
|
||||||
# may change to a JEDI cloud
|
# may change to a JEDI cloud
|
||||||
self.cloud = AZURE_PUBLIC_CLOUD
|
self.cloud = AZURE_PUBLIC_CLOUD
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from atst.domain.application_roles import ApplicationRoles
|
|||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.invitations import ApplicationInvitations
|
from atst.domain.invitations import ApplicationInvitations
|
||||||
from atst.domain.common import Paginator
|
from atst.domain.common import Paginator
|
||||||
from atst.domain.csp.cloud import GeneralCSPException
|
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.models.application_role import Status as ApplicationRoleStatus
|
from atst.models.application_role import Status as ApplicationRoleStatus
|
||||||
from atst.models.environment_role import CSPRole, EnvironmentRole
|
from atst.models.environment_role import CSPRole, EnvironmentRole
|
||||||
|
Loading…
x
Reference in New Issue
Block a user