# Conflicts:
#	azure-cli.pyproj
#	src/azure/cli/tests/test_commands.py
This commit is contained in:
Travis Prescott 2016-04-06 10:25:25 -07:00
Родитель 36856e0026 5d718e6cda
Коммит 3f3627b9fd
15 изменённых файлов: 660 добавлений и 179 удалений

Просмотреть файл

@ -55,6 +55,7 @@
<Compile Include="azure\cli\tests\test_connection_verify.py" />
<Compile Include="azure\cli\tests\test_application.py" />
<Compile Include="azure\cli\tests\test_output.py" />
<Compile Include="azure\cli\_azure_env.py" />
<Compile Include="azure\cli\_debug.py" />
<Compile Include="azure\cli\tests\test_help.py" />
<Compile Include="azure\cli\tests\test_output.py" />

Просмотреть файл

@ -8,5 +8,5 @@ pyyaml==3.11
six==1.10.0
vcrpy==1.7.4
#Same as: -e git+https://github.com/yugangw-msft/azure-activedirectory-library-for-python.git@0.2.0#egg=azure-activedirectory-library-for-python
http://40.112.211.51:8080/packages/adal-0.2.0.zip
#-e git+https://github.com/yugangw-msft/azure-activedirectory-library-for-python@v2.1#egg=azure-activedirectory-library-for-python
http://40.112.211.51:8080/packages/adal-0.2.1.zip

Просмотреть файл

@ -55,7 +55,7 @@ CLASSIFIERS = [
]
DEPENDENCIES = [
'adal==0.2.0', #from internal index server.
'adal==0.2.1', #from internal index server.
'applicationinsights',
'argcomplete',
'azure==2.0.0rc1',

Просмотреть файл

@ -0,0 +1,43 @@
CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'
ENV_DEFAULT = 'AzureCloud'
ENV_US_GOVERNMENT = 'AzureUSGovernment'
ENV_CHINA = 'AzureChinaCloud'
COMMON_TENANT = 'common'
#ported from https://github.com/Azure/azure-xplat-cli/blob/dev/lib/util/profile/environment.js
class ENDPOINT_URLS: #pylint: disable=too-few-public-methods,old-style-class,no-init
MANAGEMENT = 'management'
ACTIVE_DIRECTORY_AUTHORITY = 'active_directory_authority'
_environments = {
ENV_DEFAULT: {
ENDPOINT_URLS.MANAGEMENT: 'https://management.core.windows.net/',
ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY : 'https://login.microsoftonline.com'
},
ENV_CHINA: {
ENDPOINT_URLS.MANAGEMENT: 'https://management.core.chinacloudapi.cn/',
ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY: 'https://login.chinacloudapi.cn'
},
ENV_US_GOVERNMENT: {
ENDPOINT_URLS.MANAGEMENT: 'https://management.core.usgovcloudapi.net/',
ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY: 'https://login.microsoftonline.com'
}
}
def get_env(env_name=None):
if env_name is None:
env_name = ENV_DEFAULT
elif env_name not in _environments:
raise ValueError
return _environments[env_name]
def get_authority_url(tenant=None, env_name=None):
env = get_env(env_name)
return env[ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY] + '/' + (tenant or COMMON_TENANT)
def get_management_endpoint_url(env_name=None):
env = get_env(env_name)
return env[ENDPOINT_URLS.MANAGEMENT]

Просмотреть файл

@ -1,98 +1,374 @@
import collections
from __future__ import print_function
import collections
from codecs import open as codecs_open
import json
import os.path
from msrest.authentication import BasicTokenAuthentication
from .main import CONFIG
import adal
from azure.mgmt.resource.subscriptions import (SubscriptionClient,
SubscriptionClientConfiguration)
from .main import ACCOUNT
from ._locale import L
from ._azure_env import (get_authority_url, CLIENT_ID, get_management_endpoint_url,
ENV_DEFAULT, COMMON_TENANT)
#Names below are used by azure-xplat-cli to persist account information into
#~/.azure/azureProfile.json or osx/keychainer or windows secure storage,
#which azuer-cli will share.
#Please do not rename them unless you know what you are doing.
_IS_DEFAULT_SUBSCRIPTION = 'isDefault'
_SUBSCRIPTION_ID = 'id'
_SUBSCRIPTION_NAME = 'name'
_TENANT_ID = 'tenantId'
_USER_ENTITY = 'user'
_USER_NAME = 'name'
_SUBSCRIPTIONS = 'subscriptions'
_ENVIRONMENT_NAME = 'environmentName'
_STATE = 'state'
_USER_TYPE = 'type'
_USER = 'user'
_SERVICE_PRINCIPAL = 'servicePrincipal'
_SERVICE_PRINCIPAL_ID = 'servicePrincipalId'
_SERVICE_PRINCIPAL_TENANT = 'servicePrincipalTenant'
_TOKEN_ENTRY_USER_ID = 'userId'
#This could mean real access token, or client secret of a service principal
#This naming is no good, but can't change because xplat-cli does so.
_ACCESS_TOKEN = 'accessToken'
_AUTH_CTX_FACTORY = lambda authority, cache: adal.AuthenticationContext(authority, cache=cache)
def _read_file_content(file_path):
file_text = None
if os.path.isfile(file_path):
with codecs_open(file_path, 'r', encoding='ascii') as file_to_read:
file_text = file_to_read.read()
return file_text
class Profile(object):
def __init__(self, storage=None, auth_ctx_factory=None):
self._storage = storage or ACCOUNT
factory = auth_ctx_factory or _AUTH_CTX_FACTORY
self._creds_cache = CredsCache(factory)
self._subscription_finder = SubscriptionFinder(factory, self._creds_cache.adal_token_cache)
def __init__(self, storage=CONFIG):
self._storage = storage
def find_subscriptions_on_login(self, #pylint: disable=too-many-arguments
interactive,
username,
password,
is_service_principal,
tenant):
self._creds_cache.remove_cached_creds(username)
subscriptions = []
if interactive:
subscriptions = self._subscription_finder.find_through_interactive_flow()
else:
if is_service_principal:
if not tenant:
raise ValueError(L('Please supply tenant using "--tenant"'))
subscriptions = self._subscription_finder.find_from_service_principal_id(username,
password,
tenant)
else:
subscriptions = self._subscription_finder.find_from_user_account(username, password)
if not subscriptions:
raise RuntimeError(L('No subscriptions found for this account.'))
if is_service_principal:
self._creds_cache.save_service_principal_cred(username,
password,
tenant)
if self._creds_cache.adal_token_cache.has_state_changed:
self._creds_cache.persist_cached_creds()
consolidated = Profile._normalize_properties(self._subscription_finder.user_id,
subscriptions,
is_service_principal,
ENV_DEFAULT)
self._set_subscriptions(consolidated)
return consolidated
@staticmethod
def normalize_properties(user, subscriptions):
def _normalize_properties(user, subscriptions, is_service_principal, environment):
consolidated = []
for s in subscriptions:
consolidated.append({
'id': s.id.rpartition('/')[2],
'name': s.display_name,
'state': s.state,
'user': user,
'active': False
_SUBSCRIPTION_ID: s.id.rpartition('/')[2],
_SUBSCRIPTION_NAME: s.display_name,
_STATE: s.state,
_USER_ENTITY: {
_USER_NAME: user,
_USER_TYPE: _SERVICE_PRINCIPAL if is_service_principal else _USER
},
_IS_DEFAULT_SUBSCRIPTION: False,
_TENANT_ID: s.tenant_id,
_ENVIRONMENT_NAME: environment
})
return consolidated
def set_subscriptions(self, new_subscriptions, access_token):
existing_ones = self.load_subscriptions()
active_one = next((x for x in existing_ones if x['active']), None)
active_subscription_id = active_one['id'] if active_one else None
def _set_subscriptions(self, new_subscriptions):
existing_ones = self.load_cached_subscriptions()
active_one = next((x for x in existing_ones if x.get(_IS_DEFAULT_SUBSCRIPTION)), None)
active_subscription_id = active_one[_SUBSCRIPTION_ID] if active_one else None
#merge with existing ones
dic = collections.OrderedDict((x['id'], x) for x in existing_ones)
dic.update((x['id'], x) for x in new_subscriptions)
dic = collections.OrderedDict((x[_SUBSCRIPTION_ID], x) for x in existing_ones)
dic.update((x[_SUBSCRIPTION_ID], x) for x in new_subscriptions)
subscriptions = list(dic.values())
if active_one:
new_active_one = next(
(x for x in new_subscriptions if x['id'] == active_subscription_id), None)
(x for x in new_subscriptions if x[_SUBSCRIPTION_ID] == active_subscription_id),
None)
for s in subscriptions:
s['active'] = False
s[_IS_DEFAULT_SUBSCRIPTION] = False
if not new_active_one:
new_active_one = new_subscriptions[0]
new_active_one['active'] = True
new_active_one[_IS_DEFAULT_SUBSCRIPTION] = True
else:
new_subscriptions[0]['active'] = True
new_subscriptions[0][_IS_DEFAULT_SUBSCRIPTION] = True
#before adal/python is available, persist tokens with other profile info
for s in new_subscriptions:
s['access_token'] = access_token
self._save_subscriptions(subscriptions)
def get_login_credentials(self):
subscriptions = self.load_subscriptions()
if not subscriptions:
raise ValueError('Please run login to setup account.')
active = [x for x in subscriptions if x['active']]
if len(active) != 1:
raise ValueError('Please run "account set" to select active account.')
return BasicTokenAuthentication(
{'access_token': active[0]['access_token']}), active[0]['id']
self._cache_subscriptions_to_local_storage(subscriptions)
def set_active_subscription(self, subscription_id_or_name):
subscriptions = self.load_subscriptions()
subscriptions = self.load_cached_subscriptions()
subscription_id_or_name = subscription_id_or_name.lower()
result = [x for x in subscriptions
if subscription_id_or_name == x['id'].lower() or
subscription_id_or_name == x['name'].lower()]
if subscription_id_or_name == x[_SUBSCRIPTION_ID].lower() or
subscription_id_or_name == x[_SUBSCRIPTION_NAME].lower()]
if len(result) != 1:
raise ValueError('The subscription of "{}" does not exist or has more than'
' one match.'.format(subscription_id_or_name))
for s in subscriptions:
s['active'] = False
result[0]['active'] = True
s[_IS_DEFAULT_SUBSCRIPTION] = False
result[0][_IS_DEFAULT_SUBSCRIPTION] = True
self._save_subscriptions(subscriptions)
self._cache_subscriptions_to_local_storage(subscriptions)
def logout(self, user):
subscriptions = self.load_subscriptions()
result = [x for x in subscriptions if user.lower() == x['user'].lower()]
def logout(self, user_or_sp):
subscriptions = self.load_cached_subscriptions()
result = [x for x in subscriptions
if user_or_sp.lower() == x[_USER_ENTITY][_USER_NAME].lower()]
subscriptions = [x for x in subscriptions if x not in result]
#reset the active subscription if needed
result = [x for x in subscriptions if x['active']]
result = [x for x in subscriptions if x.get(_IS_DEFAULT_SUBSCRIPTION)]
if not result and subscriptions:
subscriptions[0]['active'] = True
subscriptions[0][_IS_DEFAULT_SUBSCRIPTION] = True
self._save_subscriptions(subscriptions)
self._cache_subscriptions_to_local_storage(subscriptions)
def load_subscriptions(self):
return self._storage.get('subscriptions') or []
self._creds_cache.remove_cached_creds(user_or_sp)
def _save_subscriptions(self, subscriptions):
self._storage['subscriptions'] = subscriptions
def load_cached_subscriptions(self):
return self._storage.get(_SUBSCRIPTIONS) or []
def _cache_subscriptions_to_local_storage(self, subscriptions):
self._storage[_SUBSCRIPTIONS] = subscriptions
def get_login_credentials(self):
subscriptions = self.load_cached_subscriptions()
if not subscriptions:
raise ValueError('Please run login to setup account.')
active = [x for x in subscriptions if x.get(_IS_DEFAULT_SUBSCRIPTION)]
if len(active) != 1:
raise ValueError('Please run "account set" to select active account.')
active_account = active[0]
user_type = active_account[_USER_ENTITY][_USER_TYPE]
username_or_sp_id = active_account[_USER_ENTITY][_USER_NAME]
if user_type == _USER:
access_token = self._creds_cache.retrieve_token_for_user(username_or_sp_id,
active_account[_TENANT_ID])
else:
access_token = self._creds_cache.retrieve_token_for_service_principal(
username_or_sp_id)
return BasicTokenAuthentication(
{'access_token': access_token}), active_account[_SUBSCRIPTION_ID]
class SubscriptionFinder(object):
'''finds all subscriptions for a user or service principal'''
def __init__(self, auth_context_factory, adal_token_cache, arm_client_factory=None):
self._adal_token_cache = adal_token_cache
self._auth_context_factory = auth_context_factory
self._resource = get_management_endpoint_url(ENV_DEFAULT)
self.user_id = None # will figure out after log user in
self._arm_client_factory = arm_client_factory or \
(lambda config: SubscriptionClient(config)) #pylint: disable=unnecessary-lambda
def find_from_user_account(self, username, password):
context = self._create_auth_context(COMMON_TENANT)
token_entry = context.acquire_token_with_username_password(
self._resource,
username,
password,
CLIENT_ID)
self.user_id = token_entry[_TOKEN_ENTRY_USER_ID]
result = self._find_using_common_tenant(token_entry[_ACCESS_TOKEN])
return result
def find_through_interactive_flow(self):
context = self._create_auth_context(COMMON_TENANT)
code = context.acquire_user_code(self._resource, CLIENT_ID)
print(code['message'])
token_entry = context.acquire_token_with_device_code(self._resource, code, CLIENT_ID)
self.user_id = token_entry[_TOKEN_ENTRY_USER_ID]
result = self._find_using_common_tenant(token_entry[_ACCESS_TOKEN])
return result
def find_from_service_principal_id(self, client_id, secret, tenant):
context = self._create_auth_context(tenant, False)
token_entry = context.acquire_token_with_client_credentials(
self._resource,
client_id,
secret)
self.user_id = client_id
result = self._find_using_specific_tenant(tenant, token_entry[_ACCESS_TOKEN])
return result
def _create_auth_context(self, tenant, use_token_cache=True):
token_cache = self._adal_token_cache if use_token_cache else None
authority = get_authority_url(tenant, ENV_DEFAULT)
return self._auth_context_factory(authority, token_cache)
def _find_using_common_tenant(self, access_token):
all_subscriptions = []
token_credential = BasicTokenAuthentication({'access_token': access_token})
client = self._arm_client_factory(SubscriptionClientConfiguration(token_credential))
tenants = client.tenants.list()
for t in tenants:
tenant_id = t.tenant_id
temp_context = self._create_auth_context(tenant_id)
temp_credentials = temp_context.acquire_token(self._resource, self.user_id, CLIENT_ID)
subscriptions = self._find_using_specific_tenant(
tenant_id,
temp_credentials[_ACCESS_TOKEN])
all_subscriptions.extend(subscriptions)
return all_subscriptions
def _find_using_specific_tenant(self, tenant, access_token):
token_credential = BasicTokenAuthentication({'access_token': access_token})
client = self._arm_client_factory(SubscriptionClientConfiguration(token_credential))
subscriptions = client.subscriptions.list()
all_subscriptions = []
for s in subscriptions:
setattr(s, 'tenant_id', tenant)
all_subscriptions.append(s)
return all_subscriptions
class CredsCache(object):
'''Caches AAD tokena and service principal secrets, and persistence will
also be handled
'''
def __init__(self, auth_ctx_factory=None):
self._token_file = os.path.expanduser('~/.azure/accessTokens.json')
self._service_principal_creds = []
self._auth_ctx_factory = auth_ctx_factory or _AUTH_CTX_FACTORY
self.adal_token_cache = None
self._load_creds()
self._resource = get_management_endpoint_url(ENV_DEFAULT)
def persist_cached_creds(self):
#be compatible with azure-xplat-cli, use 'ascii' so to save w/o a BOM
with codecs_open(self._token_file, 'w', encoding='ascii') as cred_file:
items = self.adal_token_cache.read_items()
all_creds = [entry for _, entry in items]
all_creds.extend(self._service_principal_creds)
cred_file.write(json.dumps(all_creds))
self.adal_token_cache.has_state_changed = False
def retrieve_token_for_user(self, username, tenant):
authority = get_authority_url(tenant, ENV_DEFAULT)
context = self._auth_ctx_factory(authority, cache=self.adal_token_cache)
token_entry = context.acquire_token(self._resource, username, CLIENT_ID)
if not token_entry: #TODO: consider to letting adal-python throw
raise ValueError('Could not retrieve token from local cache, please run \'login\'.')
if self.adal_token_cache.has_state_changed:
self.persist_cached_creds()
return token_entry[_ACCESS_TOKEN]
def retrieve_token_for_service_principal(self, sp_id):
matched = [x for x in self._service_principal_creds if sp_id == x[_SERVICE_PRINCIPAL_ID]]
if not matched:
raise ValueError(L('Please run "account set" to select active account.'))
cred = matched[0]
authority_url = get_authority_url(cred[_SERVICE_PRINCIPAL_TENANT], ENV_DEFAULT)
context = self._auth_ctx_factory(authority_url, None)
token_entry = context.acquire_token_with_client_credentials(self._resource,
sp_id,
cred[_ACCESS_TOKEN])
return token_entry[_ACCESS_TOKEN]
def _load_creds(self):
if self.adal_token_cache is not None:
return self.adal_token_cache
json_text = _read_file_content(self._token_file)
if json_text:
json_text = json_text.replace('\n', '')
else:
json_text = '[]'
all_entries = json.loads(json_text)
self._load_service_principal_creds(all_entries)
real_token = [x for x in all_entries if x not in self._service_principal_creds]
self.adal_token_cache = adal.TokenCache(json.dumps(real_token))
return self.adal_token_cache
def save_service_principal_cred(self, client_id, secret, tenant):
entry = {
_SERVICE_PRINCIPAL_ID: client_id,
_SERVICE_PRINCIPAL_TENANT: tenant,
_ACCESS_TOKEN: secret
}
matched = [x for x in self._service_principal_creds
if client_id == x[_SERVICE_PRINCIPAL_ID] and
tenant == x[_SERVICE_PRINCIPAL_TENANT]]
state_changed = False
if matched:
if matched[0][_ACCESS_TOKEN] != secret:
matched[0] = entry
state_changed = True
else:
self._service_principal_creds.append(entry)
state_changed = True
if state_changed:
self.persist_cached_creds()
def _load_service_principal_creds(self, creds):
for c in creds:
if c.get(_SERVICE_PRINCIPAL_ID):
self._service_principal_creds.append(c)
return self._service_principal_creds
def remove_cached_creds(self, user_or_sp):
state_changed = False
#clear AAD tokens
tokens = self.adal_token_cache.find({_TOKEN_ENTRY_USER_ID: user_or_sp})
if tokens:
state_changed = True
self.adal_token_cache.remove(tokens)
#clear service principal creds
matched = [x for x in self._service_principal_creds
if x[_SERVICE_PRINCIPAL_ID] == user_or_sp]
if matched:
state_changed = True
self._service_principal_creds = [x for x in self._service_principal_creds
if x not in matched]
if state_changed:
self.persist_cached_creds()

Просмотреть файл

@ -16,9 +16,10 @@ class Session(collections.MutableMapping):
be followed by a call to `save_with_retry` or `save`.
'''
def __init__(self):
def __init__(self, encoding=None):
self.filename = None
self.data = {}
self._encoding = encoding if encoding else 'utf-8-sig'
def load(self, filename, max_age=0):
self.filename = filename
@ -28,14 +29,14 @@ class Session(collections.MutableMapping):
st = os.stat(self.filename)
if st.st_mtime + max_age < time.clock():
self.save()
with codecs_open(self.filename, 'r', encoding='utf-8-sig') as f:
with codecs_open(self.filename, 'r', encoding=self._encoding) as f:
self.data = json.load(f)
except (OSError, IOError):
self.save()
def save(self):
if self.filename:
with codecs_open(self.filename, 'w', encoding='utf-8-sig') as f:
with codecs_open(self.filename, 'w', encoding=self._encoding) as f:
json.dump(self.data, f)
def save_with_retry(self, retries=5):

Просмотреть файл

@ -5,7 +5,7 @@ from .._locale import L
command_table = CommandTable()
@command_table.command('account list', description=L('List the imported subscriptions.'))
def list_subscriptions(args, unexpected): #pylint: disable=unused-argument
def list_subscriptions(_):
"""
type: command
long-summary: |
@ -17,7 +17,7 @@ def list_subscriptions(args, unexpected): #pylint: disable=unused-argument
text: example details
"""
profile = Profile()
subscriptions = profile.load_subscriptions()
subscriptions = profile.load_cached_subscriptions()
return subscriptions

Просмотреть файл

@ -1,12 +1,3 @@
from __future__ import print_function
from msrest.authentication import BasicTokenAuthentication
from adal import (acquire_token_with_username_password,
acquire_token_with_client_credentials,
acquire_user_code,
acquire_token_with_device_code)
from azure.mgmt.resource.subscriptions import (SubscriptionClient,
SubscriptionClientConfiguration)
from .._profile import Profile
from ..commands import CommandTable
#TODO: update adal-python to support it
@ -22,12 +13,14 @@ command_table = CommandTable()
@command_table.option('--password -p',
help=L('user password or client secret, will prompt if not given.'))
@command_table.option('--service-principal',
action='store_true',
help=L('the credential represents a service principal.'))
@command_table.option('--tenant -t', help=L('the tenant associated with the service principal.'))
def login(args):
interactive = False
username = args.get('username')
password = None
if username:
password = args.get('password')
if not password:
@ -36,41 +29,14 @@ def login(args):
else:
interactive = True
is_service_principal = args.get('service-principal')
tenant = args.get('tenant')
authority = _get_authority_url(tenant)
if interactive:
user_code = acquire_user_code(authority)
print(user_code['message'])
credentials = acquire_token_with_device_code(authority, user_code)
username = credentials['userId']
else:
if args.get('service-principal'):
if not tenant:
raise ValueError(L('Please supply tenant using "--tenant"'))
credentials = acquire_token_with_client_credentials(
authority,
username,
password)
else:
credentials = acquire_token_with_username_password(
authority,
username,
password)
token_credential = BasicTokenAuthentication({'access_token': credentials['accessToken']})
client = SubscriptionClient(SubscriptionClientConfiguration(token_credential))
subscriptions = client.subscriptions.list()
if not subscriptions:
raise RuntimeError(L('No subscriptions found for this account.'))
#keep useful properties and not json serializable
profile = Profile()
consolidated = Profile.normalize_properties(username, subscriptions)
profile.set_subscriptions(consolidated, credentials['accessToken'])
subscriptions = profile.find_subscriptions_on_login(
interactive,
username,
password,
is_service_principal,
tenant)
return list(subscriptions)
def _get_authority_url(tenant=None):
return 'https://login.microsoftonline.com/{}'.format(tenant or 'common')

Просмотреть файл

@ -7,6 +7,10 @@ from ._logging import configure_logging, logger
from ._session import Session
from ._output import OutputProducer
#ACCOUNT contains subscriptions information
# this file will be shared with azure-xplat-cli, which assumes ascii
ACCOUNT = Session('ascii')
# CONFIG provides external configuration options
CONFIG = Session()
@ -14,8 +18,12 @@ CONFIG = Session()
SESSION = Session()
def main(args, file=sys.stdout): #pylint: disable=redefined-builtin
CONFIG.load(os.path.expanduser('~/az.json'))
SESSION.load(os.path.expanduser('~/az.sess'), max_age=3600)
azure_folder = os.path.expanduser('~/.azure')
if not os.path.exists(azure_folder):
os.makedirs(azure_folder)
ACCOUNT.load(os.path.join(azure_folder, 'azureProfile.json'))
CONFIG.load(os.path.join(azure_folder, 'az.json'))
SESSION.load(os.path.join(azure_folder, 'az.sess'), max_age=3600)
configure_logging(args, CONFIG)

Просмотреть файл

@ -5,15 +5,25 @@ interactions:
Accept-Encoding: [identity]
Connection: [keep-alive]
User-Agent: [Azure-Storage/0.30.0 (Python CPython 3.5.1; Windows 10)]
x-ms-date: ['Tue, 05 Apr 2016 23:25:43 GMT']
x-ms-date: ['Wed, 06 Apr 2016 17:23:28 GMT']
x-ms-version: ['2015-04-05']
method: HEAD
uri: https://travistestresourcegr3014.blob.core.windows.net/testcontainer1234/testblob1
response:
body: {string: ''}
headers:
Date: ['Tue, 05 Apr 2016 23:25:43 GMT']
Accept-Ranges: [bytes]
Content-Length: ['1328']
Content-MD5: [OsIq1b99LRzuhyybmkSywA==]
Content-Type: [application/octet-stream]
Date: ['Wed, 06 Apr 2016 17:23:28 GMT']
ETag: ['"0x8D35DAB16B93ECE"']
Last-Modified: ['Tue, 05 Apr 2016 23:36:19 GMT']
Server: [Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0]
x-ms-blob-type: [BlockBlob]
x-ms-lease-state: [available]
x-ms-lease-status: [unlocked]
x-ms-version: ['2015-04-05']
status: {code: 404, message: The specified blob does not exist.}
x-ms-write-protection: ['false']
status: {code: 200, message: OK}
version: 1

Просмотреть файл

@ -5,21 +5,24 @@ interactions:
Accept-Encoding: [identity]
Connection: [keep-alive]
User-Agent: [Azure-Storage/0.30.0 (Python CPython 3.5.1; Windows 10)]
x-ms-date: ['Tue, 05 Apr 2016 23:25:57 GMT']
x-ms-date: ['Wed, 06 Apr 2016 17:24:56 GMT']
x-ms-version: ['2015-04-05']
method: GET
uri: https://travistestresourcegr3014.blob.core.windows.net/testcontainer1234?restype=container&comp=list
response:
body: {string: "\uFEFF<?xml version=\"1.0\" encoding=\"utf-8\"?><EnumerationResults\
\ ServiceEndpoint=\"https://travistestresourcegr3014.blob.core.windows.net/\"\
\ ContainerName=\"testcontainer1234\"><Blobs><Blob><Name>testblobl</Name><Properties><Last-Modified>Thu,\
\ ContainerName=\"testcontainer1234\"><Blobs><Blob><Name>testblob1</Name><Properties><Last-Modified>Tue,\
\ 05 Apr 2016 23:36:19 GMT</Last-Modified><Etag>0x8D35DAB16B93ECE</Etag><Content-Length>1328</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding\
\ /><Content-Language /><Content-MD5>OsIq1b99LRzuhyybmkSywA==</Content-MD5><Cache-Control\
\ /><Content-Disposition /><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState></Properties></Blob><Blob><Name>testblobl</Name><Properties><Last-Modified>Thu,\
\ 24 Mar 2016 16:04:34 GMT</Last-Modified><Etag>0x8D353FDFE4824F8</Etag><Content-Length>89344384</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding\
\ /><Content-Language /><Content-MD5 /><Cache-Control /><Content-Disposition\
\ /><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState></Properties></Blob></Blobs><NextMarker\
\ /></EnumerationResults>"}
headers:
Content-Type: [application/xml]
Date: ['Tue, 05 Apr 2016 23:25:57 GMT']
Date: ['Wed, 06 Apr 2016 17:24:55 GMT']
Server: [Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0]
x-ms-version: ['2015-04-05']
status: {code: 200, message: OK}

Просмотреть файл

@ -5,7 +5,7 @@ interactions:
Accept-Encoding: [identity]
Connection: [keep-alive]
User-Agent: [Azure-Storage/0.30.0 (Python CPython 3.5.1; Windows 10)]
x-ms-date: ['Tue, 05 Apr 2016 23:27:52 GMT']
x-ms-date: ['Wed, 06 Apr 2016 17:23:37 GMT']
x-ms-version: ['2015-04-05']
method: HEAD
uri: https://travistestresourcegr3014.blob.core.windows.net/testcontainer1234/testblob1
@ -16,13 +16,14 @@ interactions:
Content-Length: ['1328']
Content-MD5: [OsIq1b99LRzuhyybmkSywA==]
Content-Type: [application/octet-stream]
Date: ['Tue, 05 Apr 2016 23:27:53 GMT']
ETag: ['"0x8D35DA9E623241D"']
Last-Modified: ['Tue, 05 Apr 2016 23:27:48 GMT']
Date: ['Wed, 06 Apr 2016 17:23:37 GMT']
ETag: ['"0x8D35DAB16B93ECE"']
Last-Modified: ['Tue, 05 Apr 2016 23:36:19 GMT']
Server: [Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0]
x-ms-blob-type: [BlockBlob]
x-ms-lease-state: [available]
x-ms-lease-status: [unlocked]
x-ms-version: ['2015-04-05']
x-ms-write-protection: ['false']
status: {code: 200, message: OK}
version: 1

Просмотреть файл

@ -12,9 +12,9 @@
"command_specs.test_spec_storage.storage_account_usage": "Current Value : 19\nLimit : 100\nUnit : Count\nName :\n Localized Value : Storage Accounts\n Value : StorageAccounts\n\n\n",
"command_specs.test_spec_storage.storage_blob_delete": "",
"command_specs.test_spec_storage.storage_blob_download": "",
"command_specs.test_spec_storage.storage_blob_exists": "False\n\n\n",
"command_specs.test_spec_storage.storage_blob_list": "Next Marker : \nItems :\n Content : None\n Metadata : None\n Name : testblobl\n Snapshot : None\n Properties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 89344384\n Etag : 0x8D353FDFE4824F8\n Last Modified : 2016-03-24T16:04:34+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : None\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
"command_specs.test_spec_storage.storage_blob_show": "Content : None\nName : testblob1\nSnapshot : None\nMetadata :\n None\nProperties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 1328\n Etag : \"0x8D35DA9E623241D\"\n Last Modified : 2016-04-05T23:27:48+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : OsIq1b99LRzuhyybmkSywA==\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
"command_specs.test_spec_storage.storage_blob_exists": "True\n\n\n",
"command_specs.test_spec_storage.storage_blob_list": "Next Marker : \nItems :\n Content : None\n Metadata : None\n Name : testblob1\n Snapshot : None\n Properties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 1328\n Etag : 0x8D35DAB16B93ECE\n Last Modified : 2016-04-05T23:36:19+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : OsIq1b99LRzuhyybmkSywA==\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n Content : None\n Metadata : None\n Name : testblobl\n Snapshot : None\n Properties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 89344384\n Etag : 0x8D353FDFE4824F8\n Last Modified : 2016-03-24T16:04:34+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : None\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
"command_specs.test_spec_storage.storage_blob_show": "Content : None\nName : testblob1\nSnapshot : None\nMetadata :\n None\nProperties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 1328\n Etag : \"0x8D35DAB16B93ECE\"\n Last Modified : 2016-04-05T23:36:19+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : OsIq1b99LRzuhyybmkSywA==\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
"command_specs.test_spec_storage.storage_blob_upload_block_blob": "",
"command_specs.test_spec_storage.storage_container_create": "",
"command_specs.test_spec_storage.storage_container_delete": "",

Просмотреть файл

@ -59,11 +59,23 @@ my_vcr = vcr.VCR(
class TestSequenceMeta(type):
def __new__(mcs, name, bases, dict):
def gen_test(test_name, command, expected_result):
def load_subscriptions_mock(self):
return [{"id": "00000000-0000-0000-0000-000000000000", "user": "example@example.com", "access_token": "access_token", "state": "Enabled", "name": "Example", "active": True}];
return [{
"id": "00000000-0000-0000-0000-000000000000",
"user": {
"name": "example@example.com",
"type": "user"
},
"state": "Enabled",
"name": "Example",
"tenantId": "123",
"isDefault": True}]
def get_user_access_token_mock(_, _1, _2):
return 'top-secret-token-for-you'
def _test_impl(self, expected_result):
""" Test implementation, augmented with prompted recording of expected result
@ -105,7 +117,8 @@ class TestSequenceMeta(type):
if cassette_found and expected_result != None:
# playback mode - can be fully automated
@mock.patch('azure.cli._profile.Profile.load_subscriptions', load_subscriptions_mock)
@mock.patch('azure.cli._profile.Profile.load_cached_subscriptions', load_subscriptions_mock)
@mock.patch('azure.cli._profile.CredsCache.retrieve_token_for_user',get_user_access_token_mock)
@my_vcr.use_cassette(cassette_path, filter_headers=FILTER_HEADERS)
def test(self):
_test_impl(self, expected_result)
@ -122,12 +135,12 @@ class TestSequenceMeta(type):
@my_vcr.use_cassette(cassette_path, filter_headers=FILTER_HEADERS)
def test(self):
_test_impl(self, expected_result)
return test
else:
# yaml file failed to delete or bug exists
raise RuntimeError('Unable to generate test for {} due to inconsistent data. ' \
+ 'Please manually remove the associated .yaml cassette and/or the test\'s ' \
+ 'entry in expected_results.res and try again.')
return test
try:
with open(EXPECTED_RESULTS_PATH, 'r') as file:
@ -139,7 +152,7 @@ class TestSequenceMeta(type):
test_name = 'test_{}'.format(test_def['test_name'])
command = test_def['command']
expected_result = TEST_EXPECTED.get(test_path, None)
dict[test_name] = gen_test(test_path, command, expected_result)
return type.__new__(mcs, name, bases, dict)

Просмотреть файл

@ -1,18 +1,37 @@
import json
import unittest
from azure.cli._profile import Profile
import mock
import adal
from azure.cli._profile import Profile, CredsCache, SubscriptionFinder, _AUTH_CTX_FACTORY
from azure.cli._azure_env import ENV_DEFAULT
class Test_Profile(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tenant_id = 'microsoft.com'
cls.user1 = 'foo@foo.com'
cls.id1 = 'subscriptions/1'
cls.display_name1 = 'foo account'
cls.state1 = 'enabled'
cls.subscription1 = SubscriptionStub(cls.id1,
cls.display_name1,
cls.state1)
cls.token1 = 'token1'
cls.state1,
cls.tenant_id)
cls.raw_token1 = 'some...secrets'
cls.token_entry1 = {
"_clientId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
"resource": "https://management.core.windows.net/",
"tokenType": "Bearer",
"expiresOn": "2016-03-31T04:26:56.610Z",
"expiresIn": 3599,
"identityProvider": "live.com",
"_authority": "https://login.microsoftonline.com/common",
"isMRRT": True,
"refreshToken": "faked123",
"accessToken": cls.raw_token1,
"userId": cls.user1
}
cls.user2 = 'bar@bar.com'
cls.id2 = 'subscriptions/2'
@ -20,122 +39,262 @@ class Test_Profile(unittest.TestCase):
cls.state2 = 'suspended'
cls.subscription2 = SubscriptionStub(cls.id2,
cls.display_name2,
cls.state2)
cls.token2 = 'token2'
cls.state2,
cls.tenant_id)
def test_normalize(self):
consolidated = Profile.normalize_properties(self.user1,
[self.subscription1])
self.assertEqual(consolidated[0], {
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False,
ENV_DEFAULT)
expected = {
'environmentName': 'AzureCloud',
'id': '1',
'name': self.display_name1,
'state': self.state1,
'user': self.user1,
'active': False
})
'user': {
'name':self.user1,
'type':'user'
},
'isDefault': False,
'tenantId': self.tenant_id
}
self.assertEqual(expected, consolidated[0])
def test_update_add_two_different_subscriptions(self):
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
#add the first and verify
consolidated = Profile.normalize_properties(self.user1,
[self.subscription1])
profile.set_subscriptions(consolidated, self.token1)
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
self.assertEqual(len(storage_mock['subscriptions']), 1)
subscription1 = storage_mock['subscriptions'][0]
self.assertEqual(subscription1, {
self.assertEqual(subscription1, {
'environmentName': 'AzureCloud',
'id': '1',
'name': self.display_name1,
'state': self.state1,
'user': self.user1,
'access_token': self.token1,
'active': True
'user': {
'name': self.user1,
'type': 'user'
},
'isDefault': True,
'tenantId': self.tenant_id
})
#add the second and verify
consolidated = Profile.normalize_properties(self.user2,
[self.subscription2])
profile.set_subscriptions(consolidated, self.token2)
consolidated = Profile._normalize_properties(self.user2,
[self.subscription2],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
self.assertEqual(len(storage_mock['subscriptions']), 2)
subscription2 = storage_mock['subscriptions'][1]
self.assertEqual(subscription2, {
self.assertEqual(subscription2, {
'environmentName': 'AzureCloud',
'id': '2',
'name': self.display_name2,
'state': self.state2,
'user': self.user2,
'access_token': self.token2,
'active': True
'user': {
'name': self.user2,
'type': 'user'
},
'isDefault': True,
'tenantId': self.tenant_id
})
#verify the old one stays, but no longer active
self.assertEqual(storage_mock['subscriptions'][0]['name'],
subscription1['name'])
self.assertEqual(storage_mock['subscriptions'][0]['access_token'],
self.token1)
self.assertFalse(storage_mock['subscriptions'][0]['active'])
self.assertFalse(storage_mock['subscriptions'][0]['isDefault'])
def test_update_with_same_subscription_added_twice(self):
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
#add one twice and verify we will have one but with new token
consolidated = Profile.normalize_properties(self.user1,
[self.subscription1])
profile.set_subscriptions(consolidated, self.token1)
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
new_subscription1 = SubscriptionStub(self.id1,
self.display_name1,
self.state1)
consolidated = Profile.normalize_properties(self.user1,
[new_subscription1])
profile.set_subscriptions(consolidated, self.token2)
self.state1,
self.tenant_id)
consolidated = Profile._normalize_properties(self.user1,
[new_subscription1],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
self.assertEqual(len(storage_mock['subscriptions']), 1)
self.assertEqual(storage_mock['subscriptions'][0]['access_token'],
self.token2)
self.assertTrue(storage_mock['subscriptions'][0]['active'])
self.assertTrue(storage_mock['subscriptions'][0]['isDefault'])
def test_set_active_subscription(self):
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
consolidated = Profile.normalize_properties(self.user1,
[self.subscription1])
profile.set_subscriptions(consolidated, self.token1)
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
consolidated = Profile.normalize_properties(self.user2,
[self.subscription2])
profile.set_subscriptions(consolidated, self.token2)
consolidated = profile._normalize_properties(self.user2,
[self.subscription2],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
subscription1 = storage_mock['subscriptions'][0]
subscription2 = storage_mock['subscriptions'][1]
self.assertTrue(subscription2['active'])
self.assertTrue(subscription2['isDefault'])
profile.set_active_subscription(subscription1['id'])
self.assertFalse(subscription2['active'])
self.assertTrue(subscription1['active'])
self.assertFalse(subscription2['isDefault'])
self.assertTrue(subscription1['isDefault'])
def test_get_login_credentials(self):
@mock.patch('azure.cli._profile._read_file_content', return_value=None)
def test_create_token_cache(self, mock_read_file):
profile = Profile()
cache = profile._creds_cache.adal_token_cache
self.assertFalse(cache.read_items())
self.assertTrue(mock_read_file.called)
@mock.patch('azure.cli._profile._read_file_content', autospec=True)
def test_load_cached_tokens(self, mock_read_file):
mock_read_file.return_value = json.dumps([Test_Profile.token_entry1])
profile = Profile()
cache = profile._creds_cache.adal_token_cache
matched = cache.find({
"_authority": "https://login.microsoftonline.com/common",
"_clientId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
"userId": self.user1
})
self.assertEqual(len(matched), 1)
self.assertEqual(matched[0]['accessToken'], self.raw_token1)
@mock.patch('azure.cli._profile._read_file_content', autospec=True)
@mock.patch('azure.cli._profile.CredsCache.retrieve_token_for_user', autospec=True)
def test_get_login_credentials(self, mock_get_token, mock_read_cred_file):
mock_read_cred_file.return_value = json.dumps([Test_Profile.token_entry1])
mock_get_token.return_value = Test_Profile.raw_token1
#setup
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
consolidated = Profile.normalize_properties(self.user1,
[self.subscription1])
profile.set_subscriptions(consolidated, self.token1)
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
#action
cred, subscription_id = profile.get_login_credentials()
self.assertEqual(cred.token['access_token'], self.token1)
#verify
self.assertEqual(subscription_id, '1')
self.assertEqual(cred.token['access_token'], self.raw_token1)
self.assertEqual(mock_read_cred_file.call_count, 1)
self.assertEqual(mock_get_token.call_count, 1)
@mock.patch('azure.cli._profile._read_file_content', autospec=True)
@mock.patch('azure.cli._profile.CredsCache.persist_cached_creds', autospec=True)
def test_logout(self, mock_persist_creds, mock_read_cred_file):
#setup
mock_read_cred_file.return_value = json.dumps([Test_Profile.token_entry1])
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False,
ENV_DEFAULT)
profile._set_subscriptions(consolidated)
self.assertEqual(1, len(storage_mock['subscriptions']))
#action
profile.logout(self.user1)
#verify
self.assertEqual(0, len(storage_mock['subscriptions']))
self.assertEqual(mock_read_cred_file.call_count, 1)
self.assertEqual(mock_persist_creds.call_count, 1)
def test_find_subscriptions_thru_username_password(self):
finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile),
None,
lambda _: ArmClientStub(Test_Profile))
subs = finder.find_from_user_account('foo', 'bar')
self.assertEqual([self.subscription1], subs)
def test_find_through_interactive_flow(self):
finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile),
None,
lambda _: ArmClientStub(Test_Profile))
subs = finder.find_through_interactive_flow()
self.assertEqual([self.subscription1], subs)
def test_find_from_service_principal_id(self):
finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile),
None,
lambda _: ArmClientStub(Test_Profile))
subs = finder.find_from_service_principal_id('my app', 'my secret', self.tenant_id)
self.assertEqual([self.subscription1], subs)
class SubscriptionStub:
def __init__(self, id, display_name, state):
def __init__(self, id, display_name, state, tenant_id):
self.id = id
self.display_name = display_name
self.state = state
self.tenant_id = tenant_id
class AuthenticationContextStub:
def __init__(self, test_profile_cls, return_token1=True):
#we need to reference some pre-defined test artifacts in Test_Profile
self._test_profile_cls = test_profile_cls
if not return_token1:
raise ValueError('Please update to return other test tokens')
def acquire_token_with_username_password(self, _, _2, _3, _4):
return self._test_profile_cls.token_entry1
def acquire_token_with_device_code(self, _, _2, _3):
return self._test_profile_cls.token_entry1
def acquire_token_with_client_credentials(self, _, _2, _3):
return self._test_profile_cls.token_entry1
def acquire_token(self, _, _2, _3):
return self._test_profile_cls.token_entry1
def acquire_user_code(self, _, _2):
return {'message': 'secret code for you'}
class ArmClientStub:
class TenantStub:
def __init__(self, tenant_id):
self.tenant_id = tenant_id
class OperationsStub:
def __init__(self, list_result):
self._list_result = list_result
def list(self):
return self._list_result
def __init__(self, test_profile_cls, use_tenant1_and_subscription1=True):
self._test_profile_cls = test_profile_cls
if use_tenant1_and_subscription1:
self.tenants = ArmClientStub.OperationsStub([ArmClientStub.TenantStub(test_profile_cls.tenant_id)])
self.subscriptions = ArmClientStub.OperationsStub([test_profile_cls.subscription1])
else:
raise ValueError('Please update to return other test subscriptions')
if __name__ == '__main__':
unittest.main()