From 8a1ed5b1936dd603d5c887b590012271a9568eaa Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 9 Dec 2019 14:00:36 -0500 Subject: [PATCH] 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. --- atst/domain/csp/cloud.py | 124 ++++++++++++++++++++------- tests/domain/cloud/test_azure_csp.py | 60 ++++++++++++- tests/mock_azure.py | 13 ++- 3 files changed, 159 insertions(+), 38 deletions(-) diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 4cd69d3c..2da48642 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -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= + 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 diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index 19ad63c8..39c6655e 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -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): diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 34d0aa53..a360df64 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -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