-
Notifications
You must be signed in to change notification settings - Fork 3.3k
{Core} Vendor track 2 subscription SDK in azure-cli-core #15711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
748e1ff
3147c99
a35910c
2e7d18e
f4cec0a
2bdd5f2
96a823c
a216f4e
83fe6a1
5ecb89a
8ff4b54
081d233
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -77,6 +77,8 @@ | |||||
|
|
||||||
| _AZ_LOGIN_MESSAGE = "Please run 'az login' to setup account." | ||||||
|
|
||||||
| _USE_VENDORED_SUBSCRIPTION_SDK = True | ||||||
|
|
||||||
|
|
||||||
| def load_subscriptions(cli_ctx, all_clouds=False, refresh=False): | ||||||
| profile = Profile(cli_ctx=cli_ctx) | ||||||
|
|
@@ -258,7 +260,7 @@ def _normalize_properties(self, user, subscriptions, is_service_principal, cert_ | |||||
| subscription_dict = { | ||||||
| _SUBSCRIPTION_ID: s.id.rpartition('/')[2], | ||||||
| _SUBSCRIPTION_NAME: display_name, | ||||||
| _STATE: s.state.value, | ||||||
| _STATE: s.state, | ||||||
| _USER_ENTITY: { | ||||||
| _USER_NAME: user, | ||||||
| _USER_TYPE: _SERVICE_PRINCIPAL if is_service_principal else _USER | ||||||
|
|
@@ -303,11 +305,17 @@ def _build_tenant_level_accounts(self, tenants): | |||||
| return result | ||||||
|
|
||||||
| def _new_account(self): | ||||||
| from azure.cli.core.profiles import ResourceType, get_sdk | ||||||
| SubscriptionType, StateType = get_sdk(self.cli_ctx, ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS, 'Subscription', | ||||||
| 'SubscriptionState', mod='models') | ||||||
| """Build an empty Subscription which will be used as a tenant account. | ||||||
| API version doesn't matter as only specified attributes are preserved by _normalize_properties.""" | ||||||
| if _USE_VENDORED_SUBSCRIPTION_SDK: | ||||||
| from azure.cli.core.vendored_sdks.subscriptions.models import Subscription | ||||||
| SubscriptionType = Subscription | ||||||
| else: | ||||||
| from azure.cli.core.profiles import ResourceType, get_sdk | ||||||
| SubscriptionType = get_sdk(self.cli_ctx, ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS, | ||||||
| 'Subscription', mod='models') | ||||||
| s = SubscriptionType() | ||||||
| s.state = StateType.enabled | ||||||
| s.state = 'Enabled' | ||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Track 2 uses Track 1: 'state': {'key': 'state', 'type': 'SubscriptionState'},Track 2: 'state': {'key': 'state', 'type': 'str'},See email: A breaking change in Enum property's deserialization |
||||||
| return s | ||||||
|
|
||||||
| def find_subscriptions_in_vm_with_msi(self, identity_id=None, allow_no_subscriptions=None): | ||||||
|
|
@@ -449,8 +457,7 @@ def _match_account(account, subscription_id, secondary_key_name, secondary_key_v | |||||
|
|
||||||
| @staticmethod | ||||||
| def _pick_working_subscription(subscriptions): | ||||||
| from azure.mgmt.resource.subscriptions.models import SubscriptionState | ||||||
| s = next((x for x in subscriptions if x.get(_STATE) == SubscriptionState.enabled.value), None) | ||||||
| s = next((x for x in subscriptions if x.get(_STATE) == 'Enabled'), None) | ||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| return s or subscriptions[0] | ||||||
|
|
||||||
| def is_tenant_level_account(self): | ||||||
|
|
@@ -821,18 +828,19 @@ def __init__(self, cli_ctx, auth_context_factory, adal_token_cache, arm_client_f | |||||
| def create_arm_client_factory(credentials): | ||||||
| if arm_client_factory: | ||||||
| return arm_client_factory(credentials) | ||||||
| from azure.cli.core.profiles._shared import get_client_class | ||||||
| from azure.cli.core.profiles import ResourceType, get_api_version | ||||||
| from azure.cli.core.commands.client_factory import configure_common_settings | ||||||
| from azure.cli.core.azclierror import CLIInternalError | ||||||
| client_type = get_client_class(ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS) | ||||||
| from azure.cli.core.commands.client_factory import _prepare_client_kwargs_track2 | ||||||
|
|
||||||
| client_type = self._get_subscription_client_class() | ||||||
| if client_type is None: | ||||||
| from azure.cli.core.azclierror import CLIInternalError | ||||||
| raise CLIInternalError("Unable to get '{}' in profile '{}'" | ||||||
| .format(ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS, cli_ctx.cloud.profile)) | ||||||
| api_version = get_api_version(cli_ctx, ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS) | ||||||
| client_kwargs = _prepare_client_kwargs_track2(cli_ctx) | ||||||
| # We don't need to change credential_scopes as 'scopes' is ignored by BasicTokenCredential anyway | ||||||
| client = client_type(credentials, api_version=api_version, | ||||||
| base_url=self.cli_ctx.cloud.endpoints.resource_manager) | ||||||
|
Comment on lines
839
to
-834
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What api-version of Subscription is used by azure-cli, the corresponding version of vendor SDK is required for azure-cli-core, right?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I know it~ I mean if a new
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| configure_common_settings(cli_ctx, client) | ||||||
| base_url=self.cli_ctx.cloud.endpoints.resource_manager, **client_kwargs) | ||||||
| return client | ||||||
|
|
||||||
| self._arm_client_factory = create_arm_client_factory | ||||||
|
|
@@ -916,16 +924,17 @@ def _create_auth_context(self, tenant, use_token_cache=True): | |||||
|
|
||||||
| def _find_using_common_tenant(self, access_token, resource): | ||||||
| import adal | ||||||
| from msrest.authentication import BasicTokenAuthentication | ||||||
| from azure.cli.core.adal_authentication import BasicTokenCredential | ||||||
|
|
||||||
| all_subscriptions = [] | ||||||
| empty_tenants = [] | ||||||
| mfa_tenants = [] | ||||||
| token_credential = BasicTokenAuthentication({'access_token': access_token}) | ||||||
| token_credential = BasicTokenCredential(access_token) | ||||||
| client = self._arm_client_factory(token_credential) | ||||||
| tenants = client.tenants.list() | ||||||
| for t in tenants: | ||||||
| tenant_id = t.tenant_id | ||||||
| logger.debug("Finding subscriptions under tenant %s", tenant_id) | ||||||
| # display_name is available since /tenants?api-version=2018-06-01, | ||||||
| # not available in /tenants?api-version=2016-06-01 | ||||||
| if not hasattr(t, 'display_name'): | ||||||
|
|
@@ -934,6 +943,7 @@ def _find_using_common_tenant(self, access_token, resource): | |||||
| t.display_name = t.additional_properties.get('displayName') | ||||||
| temp_context = self._create_auth_context(tenant_id) | ||||||
| try: | ||||||
| logger.debug("Acquiring a token with tenant=%s, resource=%s", tenant_id, resource) | ||||||
| temp_credentials = temp_context.acquire_token(resource, self.user_id, _CLIENT_ID) | ||||||
| except adal.AdalError as ex: | ||||||
| # because user creds went through the 'common' tenant, the error here must be | ||||||
|
|
@@ -990,9 +1000,9 @@ def _find_using_common_tenant(self, access_token, resource): | |||||
| return all_subscriptions | ||||||
|
|
||||||
| def _find_using_specific_tenant(self, tenant, access_token): | ||||||
| from msrest.authentication import BasicTokenAuthentication | ||||||
| from azure.cli.core.adal_authentication import BasicTokenCredential | ||||||
|
|
||||||
| token_credential = BasicTokenAuthentication({'access_token': access_token}) | ||||||
| token_credential = BasicTokenCredential(access_token) | ||||||
| client = self._arm_client_factory(token_credential) | ||||||
| subscriptions = client.subscriptions.list() | ||||||
| all_subscriptions = [] | ||||||
|
|
@@ -1005,6 +1015,21 @@ def _find_using_specific_tenant(self, tenant, access_token): | |||||
| self.tenants.append(tenant) | ||||||
| return all_subscriptions | ||||||
|
|
||||||
| def _get_subscription_client_class(self): # pylint: disable=no-self-use | ||||||
| """Get the subscription client class. It can come from either the vendored SDK or public SDK, depending | ||||||
| on the design of architecture. | ||||||
| """ | ||||||
| if _USE_VENDORED_SUBSCRIPTION_SDK: | ||||||
| # Use vendered subscription SDK to decouple from `resource` command module | ||||||
| from azure.cli.core.vendored_sdks.subscriptions import SubscriptionClient | ||||||
| client_type = SubscriptionClient | ||||||
| else: | ||||||
| # Use the public SDK | ||||||
| from azure.cli.core.profiles import ResourceType | ||||||
| from azure.cli.core.profiles._shared import get_client_class | ||||||
| client_type = get_client_class(ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS) | ||||||
| return client_type | ||||||
|
|
||||||
|
|
||||||
| class CredsCache: | ||||||
| '''Caches AAD tokena and service principal secrets, and persistence will | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,3 +171,16 @@ def _try_scopes_to_resource(scopes): | |
|
|
||
| # Exactly only one scope is provided | ||
| return scopes_to_resource(scopes) | ||
|
|
||
|
|
||
| class BasicTokenCredential: | ||
| # pylint:disable=too-few-public-methods | ||
| """A Track 2 implementation of msrest.authentication.BasicTokenAuthentication. | ||
| This credential shouldn't be used by any command module, expect azure-cli-core. | ||
| """ | ||
| def __init__(self, access_token): | ||
| self.access_token = access_token | ||
|
|
||
| def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument | ||
| # Because get_token can't refresh the access token, always mark the token as unexpired | ||
| return AccessToken(self.access_token, int(time.time() + 3600)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use a meaningful constant instead of a magic number (3600) so that reviewers can understand it easier?@[email protected]
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As Also, Subscriptions - List and Tenants - List APIs are not long-running operations, so the time of |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # coding=utf-8 | ||
| # -------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # Code generated by Microsoft (R) AutoRest Code Generator. | ||
| # Changes may cause incorrect behavior and will be lost if the code is regenerated. | ||
| # -------------------------------------------------------------------------- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # coding=utf-8 | ||
| # -------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # Code generated by Microsoft (R) AutoRest Code Generator. | ||
| # Changes may cause incorrect behavior and will be lost if the code is regenerated. | ||
| # -------------------------------------------------------------------------- | ||
|
|
||
| from ._subscription_client import SubscriptionClient | ||
| __all__ = ['SubscriptionClient'] | ||
|
|
||
| try: | ||
| from ._patch import patch_sdk # type: ignore | ||
| patch_sdk() | ||
| except ImportError: | ||
| pass |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # coding=utf-8 | ||
| # -------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for | ||
| # license information. | ||
| # | ||
| # Code generated by Microsoft (R) AutoRest Code Generator. | ||
| # Changes may cause incorrect behavior and will be lost if the code is | ||
| # regenerated. | ||
| # -------------------------------------------------------------------------- | ||
| from typing import Any | ||
|
|
||
| from azure.core.configuration import Configuration | ||
| from azure.core.pipeline import policies | ||
| from azure.mgmt.core.policies import ARMHttpLoggingPolicy | ||
|
|
||
| from ._version import VERSION | ||
|
|
||
|
|
||
| class SubscriptionClientConfiguration(Configuration): | ||
| """Configuration for SubscriptionClient. | ||
|
|
||
| Note that all parameters used to create this instance are saved as instance | ||
| attributes. | ||
|
|
||
| :param credential: Credential needed for the client to connect to Azure. | ||
| :type credential: ~azure.core.credentials.TokenCredential | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| credential, # type: "TokenCredential" | ||
| **kwargs # type: Any | ||
| ): | ||
| # type: (...) -> None | ||
| if credential is None: | ||
| raise ValueError("Parameter 'credential' must not be None.") | ||
| super(SubscriptionClientConfiguration, self).__init__(**kwargs) | ||
|
|
||
| self.credential = credential | ||
| self.credential_scopes = kwargs.pop('credential_scopes', ['https://management.azure.com/.default']) | ||
| kwargs.setdefault('sdk_moniker', 'azure-mgmt-resource/{}'.format(VERSION)) | ||
| self._configure(**kwargs) | ||
|
|
||
| def _configure( | ||
| self, | ||
| **kwargs # type: Any | ||
| ): | ||
| # type: (...) -> None | ||
| self.user_agent_policy = kwargs.get('user_agent_policy') or policies.UserAgentPolicy(**kwargs) | ||
| self.headers_policy = kwargs.get('headers_policy') or policies.HeadersPolicy(**kwargs) | ||
| self.proxy_policy = kwargs.get('proxy_policy') or policies.ProxyPolicy(**kwargs) | ||
| self.logging_policy = kwargs.get('logging_policy') or policies.NetworkTraceLoggingPolicy(**kwargs) | ||
| self.http_logging_policy = kwargs.get('http_logging_policy') or ARMHttpLoggingPolicy(**kwargs) | ||
| self.retry_policy = kwargs.get('retry_policy') or policies.RetryPolicy(**kwargs) | ||
| self.custom_hook_policy = kwargs.get('custom_hook_policy') or policies.CustomHookPolicy(**kwargs) | ||
| self.redirect_policy = kwargs.get('redirect_policy') or policies.RedirectPolicy(**kwargs) | ||
| self.authentication_policy = kwargs.get('authentication_policy') | ||
| if self.credential and not self.authentication_policy: | ||
| self.authentication_policy = policies.BearerTokenCredentialPolicy(self.credential, *self.credential_scopes, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to change this to
Falsein the future ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not.
azure-cli-coreuses only very limited functionalities from this SDK, so we can always use the fixed version.