Sketch in Management Group integration for Azure

Add mocks and real implementations for creating nested management groups that reflect the Portfolio->Application->Environment->Subscription hierarchy.
This commit is contained in:
tomdds 2019-12-09 14:00:36 -05:00 committed by dandds
parent 2801e07454
commit 8a1ed5b193
3 changed files with 159 additions and 38 deletions

View File

@ -3,6 +3,7 @@ import re
from uuid import uuid4
from atst.models.user import User
from atst.models.application import Application
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
@ -399,13 +400,14 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
class AzureSDKProvider(object):
def __init__(self):
from azure.mgmt import subscription, authorization
from azure.mgmt import subscription, authorization, managementgroups
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.managementgroups = managementgroups
self.graphrbac = graphrbac
self.credentials = credentials
# may change to a JEDI cloud
@ -428,42 +430,23 @@ class AzureCloudProvider(CloudProviderInterface):
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)
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
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
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>
management_group = self._create_management_group(
credentials, management_group_id, display_name, parent_id,
)
# 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
return management_group
def create_atat_admin_user(
self, auth_credentials: Dict, csp_environment_id: str
@ -502,6 +485,83 @@ class AzureCloudProvider(CloudProviderInterface):
"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)
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
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 _get_management_service_principal(self):
# we really should be using graph.microsoft.com, but i'm getting
# "expired token" errors for that

View File

@ -1,27 +1,81 @@
import pytest
from unittest.mock import Mock
from uuid import uuid4
from atst.domain.csp.cloud import AzureCloudProvider
from tests.mock_azure import mock_azure, AUTH_CREDENTIALS
from tests.factories import EnvironmentFactory
from tests.factories import EnvironmentFactory, ApplicationFactory
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
# 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
#
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create()
subscription_id = str(uuid4())
credentials = mock_azure._get_credential_obj(AUTH_CREDENTIALS)
display_name = "Test Subscription"
billing_profile_id = str(uuid4())
sku_id = str(uuid4())
management_group_id = (
environment.cloud_id # environment.csp_details.management_group_id?
)
billing_account_name = (
"?" # environment.application.portfilio.csp_details.billing_account.name?
)
invoice_section_name = "?" # environment.name? or something specific to billing?
mock_azure.sdk.subscription.SubscriptionClient.return_value.subscription_factory.create_subscription.return_value.result.return_value.subscription_link = (
f"subscriptions/{subscription_id}"
)
result = mock_azure._create_subscription(
credentials,
display_name,
billing_profile_id,
sku_id,
management_group_id,
billing_account_name,
invoice_section_name,
)
assert result == subscription_id
def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock(
**spec_dict
)
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure.create_environment(
AUTH_CREDENTIALS, environment.creator, environment
)
assert result == subscription_id
assert result.id == "Test Id"
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure._create_application(AUTH_CREDENTIALS, application)
assert result.id == "Test Id"
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):

View File

@ -10,9 +10,9 @@ AZURE_CONFIG = {
}
AUTH_CREDENTIALS = {
"CLIENT_ID": AZURE_CONFIG["AZURE_CLIENT_ID"],
"SECRET_KEY": AZURE_CONFIG["AZURE_SECRET_KEY"],
"TENANT_ID": AZURE_CONFIG["AZURE_TENANT_ID"],
"client_id": AZURE_CONFIG["AZURE_CLIENT_ID"],
"secret_key": AZURE_CONFIG["AZURE_SECRET_KEY"],
"tenant_id": AZURE_CONFIG["AZURE_TENANT_ID"],
}
@ -28,6 +28,12 @@ def mock_authorization():
return Mock(spec=authorization)
def mock_managementgroups():
from azure.mgmt import managementgroups
return Mock(spec=managementgroups)
def mock_graphrbac():
import azure.graphrbac as graphrbac
@ -46,6 +52,7 @@ class MockAzureSDK(object):
self.subscription = mock_subscription()
self.authorization = mock_authorization()
self.managementgroups = mock_managementgroups()
self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials()
# may change to a JEDI cloud