Add functionality for creating and verifying subscriptions.
Currently the create call will be consumed by on-demand requests from the frontend, and the 2 stage create will be used by the enviroment management group provisioning to verify an initial subscription was created.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import re
|
||||
from secrets import token_urlsafe
|
||||
from typing import Any, Dict
|
||||
from uuid import uuid4
|
||||
@@ -9,6 +8,10 @@ from atst.utils import sha256_hex
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import AuthenticationException
|
||||
from .models import (
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionCreationCSPResult,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
SuscriptionVerificationCSPResult,
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
@@ -44,10 +47,6 @@ from .models import (
|
||||
)
|
||||
from .policy import AzurePolicyManager
|
||||
|
||||
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
|
||||
# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
@@ -231,49 +230,6 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
# instead?
|
||||
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,
|
||||
):
|
||||
@@ -517,6 +473,59 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_subscription(self, payload: SubscriptionCreationCSPPayload):
|
||||
sp_token = self._get_tenant_principal_token(payload.tenant_id)
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for subscription creation"
|
||||
)
|
||||
|
||||
request_body = {
|
||||
"displayName": "Test Sub 1",
|
||||
"skuId": AZURE_SKU_ID,
|
||||
"managementGroupId": payload.parent_group_id,
|
||||
}
|
||||
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/invoiceSections/{payload.invoice_section_name}/providers/Microsoft.Subscription/createSubscription?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 in [200, 202]:
|
||||
# 202 has location/retry after headers
|
||||
return SubscriptionCreationCSPResult(**result.headers, **result.json())
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_subscription_creation(self, payload: SubscriptionCreationCSPPayload):
|
||||
return self.create_subscription(payload)
|
||||
|
||||
def create_subscription_verification(
|
||||
self, payload: SubscriptionVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_tenant_principal_token(payload.tenant_id)
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for subscription verification"
|
||||
)
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {sp_token}",
|
||||
}
|
||||
|
||||
result = self.sdk.requests.get(
|
||||
payload.subscription_verify_url, headers=auth_header
|
||||
)
|
||||
|
||||
if result.ok:
|
||||
# 202 has location/retry after headers
|
||||
return SuscriptionVerificationCSPResult(**result.json())
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload):
|
||||
mgmt_token = self._get_elevated_management_token(payload.tenant_id)
|
||||
|
||||
@@ -861,6 +870,12 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"tenant_id": self.tenant_id,
|
||||
}
|
||||
|
||||
def _get_tenant_principal_token(self, tenant_id):
|
||||
creds = self._source_creds(tenant_id)
|
||||
return self._get_sp_token(
|
||||
creds.tenant_id, creds.tenant_sp_client_id, creds.tenant_sp_key
|
||||
)
|
||||
|
||||
def _get_elevated_management_token(self, tenant_id):
|
||||
mgmt_token = self._get_tenant_admin_token(
|
||||
tenant_id, self.sdk.cloud.endpoints.resource_manager
|
||||
|
@@ -112,9 +112,3 @@ class CloudProviderInterface:
|
||||
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()
|
||||
|
@@ -27,6 +27,8 @@ from .models import (
|
||||
BillingProfileVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
@@ -109,6 +111,17 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
return csp_environment_id
|
||||
|
||||
def create_subscription(self, payload: SubscriptionCreationCSPPayload):
|
||||
pass
|
||||
|
||||
def create_subscription_creation(self, payload: SubscriptionCreationCSPPayload):
|
||||
pass
|
||||
|
||||
def create_subscription_verification(
|
||||
self, payload: SubscriptionVerificationCSPPayload
|
||||
):
|
||||
pass
|
||||
|
||||
def create_atat_admin_user(self, auth_credentials, csp_environment_id):
|
||||
self._authorize(auth_credentials)
|
||||
|
||||
@@ -382,11 +395,6 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
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"
|
||||
|
||||
|
@@ -406,3 +406,46 @@ class KeyVaultCredentials(BaseModel):
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
class SubscriptionCreationCSPPayload(BaseCSPPayload):
|
||||
parent_group_id: str
|
||||
billing_account_name: str
|
||||
billing_profile_name: str
|
||||
invoice_section_name: str
|
||||
|
||||
|
||||
class SubscriptionCreationCSPResult(AliasModel):
|
||||
subscription_verify_url: str
|
||||
subscription_retry_after: str
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"subscription_verify_url": "Location",
|
||||
"subscription_retry_after": "Retry-After",
|
||||
}
|
||||
|
||||
|
||||
class SubscriptionVerificationCSPPayload(BaseCSPPayload):
|
||||
subscription_verify_url: str
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class SuscriptionVerificationCSPResult(AliasModel):
|
||||
subscription_id: str
|
||||
|
||||
@validator("subscription_id", pre=True, always=True)
|
||||
def enforce_display_name_length(cls, sub_id):
|
||||
sub_id_match = SUBSCRIPTION_ID_REGEX.match(sub_id)
|
||||
if sub_id_match:
|
||||
return sub_id_match.group(1)
|
||||
|
||||
return False
|
||||
|
||||
class Config:
|
||||
fields = {"subscription_id": "subscriptionLink"}
|
||||
|
@@ -140,7 +140,6 @@ class PortfolioStateMachine(
|
||||
|
||||
# Accumulate payload w/ creds
|
||||
payload = event.kwargs.get("csp_data")
|
||||
payload["creds"] = event.kwargs.get("creds")
|
||||
|
||||
payload_data_cls = get_stage_csp_class(stage, "payload")
|
||||
if not payload_data_cls:
|
||||
|
@@ -14,6 +14,8 @@ from atst.domain.applications import Applications
|
||||
from atst.domain.application_roles import ApplicationRoles
|
||||
from atst.domain.audit_log import AuditLog
|
||||
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||
|
||||
from atst.domain.csp.cloud.models import SubscriptionCreationCSPPayload
|
||||
from atst.domain.common import Paginator
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.domain.invitations import ApplicationInvitations
|
||||
@@ -533,7 +535,20 @@ def create_subscription(environment_id):
|
||||
environment = Environments.get(environment_id)
|
||||
|
||||
try:
|
||||
app.csp.cloud.create_subscription(environment)
|
||||
csp_data = environment.application.portfolio.csp_data
|
||||
parent_group_id = environment.cloud_id
|
||||
invoice_section_name = csp_data["billing_profile_properties"][
|
||||
"invoice_sections"
|
||||
][0]["invoice_section_name"]
|
||||
|
||||
payload = SubscriptionCreationCSPPayload(
|
||||
tenant_id=csp_data.get("tenant_id"),
|
||||
parent_group_id=parent_group_id,
|
||||
billing_account_name=csp_data.get("billing_account_name"),
|
||||
billing_profile_name=csp_data.get("billing_profile_name"),
|
||||
invoice_section_name=invoice_section_name,
|
||||
)
|
||||
app.csp.cloud.create_subscription(payload)
|
||||
flash("environment_subscription_success", name=environment.displayname)
|
||||
|
||||
except GeneralCSPException:
|
||||
|
Reference in New Issue
Block a user