Hello community, here is the log from the commit of package azure-cli-core for openSUSE:Factory checked in at 2020-07-26 16:17:43 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/azure-cli-core (Old) and /work/SRC/openSUSE:Factory/.azure-cli-core.new.3592 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "azure-cli-core" Sun Jul 26 16:17:43 2020 rev:16 rq:821948 version:2.9.1 Changes: -------- --- /work/SRC/openSUSE:Factory/azure-cli-core/azure-cli-core.changes 2020-06-27 23:22:21.365778057 +0200 +++ /work/SRC/openSUSE:Factory/.azure-cli-core.new.3592/azure-cli-core.changes 2020-07-26 16:19:02.192791035 +0200 @@ -1,0 +2,9 @@ +Mon Jul 20 14:42:00 UTC 2020 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- New upstream release + + Version 2.9.1 + + For detailed information about changes see the + HISTORY.txt file provided with this package +- Update Requires from setup.py + +------------------------------------------------------------------- Old: ---- azure-cli-core-2.7.0.tar.gz New: ---- azure-cli-core-2.9.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ azure-cli-core.spec ++++++ --- /var/tmp/diff_new_pack.I6ldk1/_old 2020-07-26 16:19:03.328792098 +0200 +++ /var/tmp/diff_new_pack.I6ldk1/_new 2020-07-26 16:19:03.332792101 +0200 @@ -17,7 +17,7 @@ Name: azure-cli-core -Version: 2.7.0 +Version: 2.9.1 Release: 0 Summary: Microsoft Azure CLI Core Module License: MIT @@ -42,10 +42,10 @@ Requires: python3-argcomplete >= 1.8 Requires: python3-azure-mgmt-core < 2.0.0 Requires: python3-azure-mgmt-core >= 1.0.0 -Requires: python3-azure-mgmt-resource < 10.0.0 -Requires: python3-azure-mgmt-resource >= 9.0.0 +Requires: python3-azure-mgmt-resource < 11.0.0 +Requires: python3-azure-mgmt-resource >= 10.0.0 Requires: python3-azure-nspkg >= 3.0.0 -Requires: python3-colorama >= 0.3.9 +Requires: python3-colorama >= 0.4.1 Requires: python3-humanfriendly < 9.0 Requires: python3-humanfriendly >= 4.7 Requires: python3-jmespath @@ -63,7 +63,7 @@ Requires: python3-pkginfo >= 1.5.0.1 Requires: python3-pyOpenSSL >= 17.1.0 Requires: python3-requests < 3.0.0 -Requires: python3-requests >= 2.20 +Requires: python3-requests >= 2.22 Requires: python3-six < 2.0.0 Requires: python3-six >= 1.12 Requires: python3-wheel >= 0.30.0 ++++++ azure-cli-core-2.7.0.tar.gz -> azure-cli-core-2.9.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/HISTORY.rst new/azure-cli-core-2.9.1/HISTORY.rst --- old/azure-cli-core-2.7.0/HISTORY.rst 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/HISTORY.rst 2020-07-16 10:09:55.000000000 +0200 @@ -3,6 +3,18 @@ Release History =============== +2.9.1 +++++++ +* Minor fixes + +2.9.0 +++++++ +* Fix get_token() issue in msi login and `expiresIn` key error in cloud shell login credentials for track 2 SDK related commands (#14187) + +2.8.0 +++++++ +* Add get_command_loader() entry to support to load customized CommandLoader (#13763) + 2.7.0 ++++++ * Enable local context for location (#13682) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/PKG-INFO new/azure-cli-core-2.9.1/PKG-INFO --- old/azure-cli-core-2.7.0/PKG-INFO 2020-05-29 10:06:40.000000000 +0200 +++ new/azure-cli-core-2.9.1/PKG-INFO 2020-07-16 10:10:08.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: azure-cli-core -Version: 2.7.0 +Version: 2.9.1 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation @@ -15,6 +15,18 @@ Release History =============== + 2.9.1 + ++++++ + * Minor fixes + + 2.9.0 + ++++++ + * Fix get_token() issue in msi login and `expiresIn` key error in cloud shell login credentials for track 2 SDK related commands (#14187) + + 2.8.0 + ++++++ + * Add get_command_loader() entry to support to load customized CommandLoader (#13763) + 2.7.0 ++++++ * Enable local context for location (#13682) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/__init__.py new/azure-cli-core-2.9.1/azure/cli/core/__init__.py --- old/azure-cli-core-2.7.0/azure/cli/core/__init__.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/__init__.py 2020-07-16 10:09:55.000000000 +0200 @@ -6,7 +6,7 @@ from __future__ import print_function -__version__ = "2.7.0" +__version__ = "2.9.1" import os import sys @@ -31,6 +31,12 @@ 'content_version', 'kwargs', 'client', 'no_wait'] EVENT_FAILED_EXTENSION_LOAD = 'MainLoader.OnFailedExtensionLoad' +# [Reserved, in case of future usage] +# Modules that will always be loaded. They don't expose commands but hook into CLI core. +ALWAYS_LOADED_MODULES = [] +# Extensions that will always be loaded if installed. They don't expose commands but hook into CLI core. +ALWAYS_LOADED_EXTENSIONS = ['azext_ai_examples', 'azext_ai_did_you_mean_this'] + class AzCli(CLI): @@ -42,7 +48,7 @@ register_ids_argument, register_global_subscription_argument) from azure.cli.core.cloud import get_active_cloud from azure.cli.core.commands.transform import register_global_transforms - from azure.cli.core._session import ACCOUNT, CONFIG, SESSION + from azure.cli.core._session import ACCOUNT, CONFIG, SESSION, INDEX from knack.util import ensure_dir @@ -57,6 +63,8 @@ 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) + INDEX.load(os.path.join(azure_folder, 'commandIndex.json')) + self.cloud = get_active_cloud(self) logger.debug('Current cloud config:\n%s', str(self.cloud.name)) self.local_context = AzCLILocalContext(self) @@ -143,11 +151,18 @@ args_str = [] for name, value in local_context_args: args_str.append('{}: {}'.format(name, value)) - logger.warning('Command argument values saved to local context: %s', ', '.join(args_str)) + logger.warning('Your preference of %s now saved to local context. To learn more, type in `az ' + 'local-context --help`', ', '.join(args_str) + ' is' if len(args_str) == 1 else ' are') class MainCommandsLoader(CLICommandsLoader): + # Format string for pretty-print the command module table + header_mod = "%-20s %10s %9s %9s" % ("Name", "Load Time", "Groups", "Commands") + item_format_string = "%-20s %10.3f %9d %9d" + header_ext = header_mod + " Directory" + item_ext_format_string = item_format_string + " %s" + def __init__(self, cli_ctx=None): super(MainCommandsLoader, self).__init__(cli_ctx) self.cmd_to_loader_map = {} @@ -160,33 +175,48 @@ loader.command_table = self.command_table loader._update_command_definitions() # pylint: disable=protected-access - # pylint: disable=too-many-statements + # pylint: disable=too-many-statements, too-many-locals def load_command_table(self, args): from importlib import import_module import pkgutil import traceback from azure.cli.core.commands import ( - _load_module_command_loader, _load_extension_command_loader, BLACKLISTED_MODS, ExtensionCommandSource) + _load_module_command_loader, _load_extension_command_loader, BLOCKED_MODS, ExtensionCommandSource) from azure.cli.core.extension import ( get_extensions, get_extension_path, get_extension_modname) - def _update_command_table_from_modules(args): - '''Loads command table(s) - When `module_name` is specified, only commands from that module will be loaded. - If the module is not found, all commands are loaded. - ''' - installed_command_modules = [] - try: - mods_ns_pkg = import_module('azure.cli.command_modules') - installed_command_modules = [modname for _, modname, _ in - pkgutil.iter_modules(mods_ns_pkg.__path__) - if modname not in BLACKLISTED_MODS] - except ImportError as e: - logger.warning(e) + def _update_command_table_from_modules(args, command_modules=None): + """Loads command tables from modules and merge into the main command table. + + :param args: Arguments of the command. + :param list command_modules: Command modules to load, in the format like ['resource', 'profile']. + If None, will do module discovery and load all modules. + If [], only ALWAYS_LOADED_MODULES will be loaded. + Otherwise, the list will be extended using ALWAYS_LOADED_MODULES. + """ + + # As command modules are built-in, the existence of modules in ALWAYS_LOADED_MODULES is NOT checked + if command_modules is not None: + command_modules.extend(ALWAYS_LOADED_MODULES) + else: + # Perform module discovery + command_modules = [] + try: + mods_ns_pkg = import_module('azure.cli.command_modules') + command_modules = [modname for _, modname, _ in + pkgutil.iter_modules(mods_ns_pkg.__path__)] + logger.debug('Discovered command modules: %s', command_modules) + except ImportError as e: + logger.warning(e) - logger.debug('Installed command modules %s', installed_command_modules) + count = 0 cumulative_elapsed_time = 0 - for mod in [m for m in installed_command_modules if m not in BLACKLISTED_MODS]: + cumulative_group_count = 0 + cumulative_command_count = 0 + logger.debug("Loading command modules:") + logger.debug(self.header_mod) + + for mod in [m for m in command_modules if m not in BLOCKED_MODS]: try: start_time = timeit.default_timer() module_command_table, module_group_table = _load_module_command_loader(self, args, mod) @@ -194,9 +224,14 @@ cmd.command_source = mod self.command_table.update(module_command_table) self.command_group_table.update(module_group_table) + elapsed_time = timeit.default_timer() - start_time - logger.debug("Loaded module '%s' in %.3f seconds.", mod, elapsed_time) + logger.debug(self.item_format_string, mod, elapsed_time, + len(module_group_table), len(module_command_table)) + count += 1 cumulative_elapsed_time += elapsed_time + cumulative_group_count += len(module_group_table) + cumulative_command_count += len(module_command_table) except Exception as ex: # pylint: disable=broad-except # Changing this error message requires updating CI script that checks for failed # module loading. @@ -205,14 +240,21 @@ telemetry.set_exception(exception=ex, fault_type='module-load-error-' + mod, summary='Error loading module: {}'.format(mod)) logger.debug(traceback.format_exc()) - logger.debug("Loaded all modules in %.3f seconds. " - "(note: there's always an overhead with the first module loaded)", - cumulative_elapsed_time) - - def _update_command_table_from_extensions(ext_suppressions): - - from azure.cli.core.extension.operations import check_version_compatibility - + # Summary line + logger.debug(self.item_format_string, + "Total ({})".format(count), cumulative_elapsed_time, + cumulative_group_count, cumulative_command_count) + + def _update_command_table_from_extensions(ext_suppressions, extension_modname=None): + """Loads command tables from extensions and merge into the main command table. + + :param ext_suppressions: Extension suppression information. + :param extension_modname: Command modules to load, in the format like ['azext_timeseriesinsights']. + If None, will do extension discovery and load all extensions. + If [], only ALWAYS_LOADED_EXTENSIONS will be loaded. + Otherwise, the list will be extended using ALWAYS_LOADED_EXTENSIONS. + If the extensions in the list are not installed, it will be skipped. + """ def _handle_extension_suppressions(extensions): filtered_extensions = [] for ext in extensions: @@ -224,13 +266,39 @@ filtered_extensions.append(ext) return filtered_extensions + def _filter_modname(extensions): + # Extension's name may not be the same as its modname. eg. name: virtual-wan, modname: azext_vwan + filtered_extensions = [] + for ext in extensions: + ext_mod = get_extension_modname(ext.name, ext.path) + # Filter the extensions according to the index + if ext_mod in extension_modname: + filtered_extensions.append(ext) + extension_modname.remove(ext_mod) + if extension_modname: + logger.debug("These extensions are not installed and will be skipped: %s", extension_modname) + return filtered_extensions + extensions = get_extensions() if extensions: - logger.debug("Found %s extensions: %s", len(extensions), [e.name for e in extensions]) + if extension_modname is not None: + extension_modname.extend(ALWAYS_LOADED_EXTENSIONS) + extensions = _filter_modname(extensions) allowed_extensions = _handle_extension_suppressions(extensions) module_commands = set(self.command_table.keys()) + + count = 0 + cumulative_elapsed_time = 0 + cumulative_group_count = 0 + cumulative_command_count = 0 + logger.debug("Loading extensions:") + logger.debug(self.header_ext) + for ext in allowed_extensions: try: + # Import in the `for` loop because `allowed_extensions` can be []. In such case we + # don't need to import `check_version_compatibility` at all. + from azure.cli.core.extension.operations import check_version_compatibility check_version_compatibility(ext.get_metadata()) except CLIError as ex: # issue warning and skip loading extensions that aren't compatible with the CLI core @@ -238,7 +306,6 @@ continue ext_name = ext.name ext_dir = ext.path or get_extension_path(ext_name) - logger.debug("Extensions directory: '%s'", ext_dir) sys.path.append(ext_dir) try: ext_mod = get_extension_modname(ext_name, ext_dir=ext_dir) @@ -258,13 +325,24 @@ self.command_table.update(extension_command_table) self.command_group_table.update(extension_group_table) + elapsed_time = timeit.default_timer() - start_time - logger.debug("Loaded extension '%s' in %.3f seconds.", ext_name, elapsed_time) + logger.debug(self.item_ext_format_string, ext_name, elapsed_time, + len(extension_group_table), len(extension_command_table), + ext_dir) + count += 1 + cumulative_elapsed_time += elapsed_time + cumulative_group_count += len(extension_group_table) + cumulative_command_count += len(extension_command_table) except Exception as ex: # pylint: disable=broad-except self.cli_ctx.raise_event(EVENT_FAILED_EXTENSION_LOAD, extension_name=ext_name) logger.warning("Unable to load extension '%s: %s'. Use --debug for more information.", ext_name, ex) logger.debug(traceback.format_exc()) + # Summary line + logger.debug(self.item_ext_format_string, + "Total ({})".format(count), cumulative_elapsed_time, + cumulative_group_count, cumulative_command_count, "") def _wrap_suppress_extension_func(func, ext): """ Wrapper method to handle centralization of log messages for extension filters """ @@ -295,15 +373,63 @@ res.append(sup) return res + def _roughly_parse_command(args): + # Roughly parse the command part: <az vm create> --name vm1 + # Similar to knack.invocation.CommandInvoker._rudimentary_get_command, but we don't need to bother with + # positional args + nouns = [] + for arg in args: + if arg and arg[0] != '-': + nouns.append(arg) + else: + break + return ' '.join(nouns).lower() + + # Clear the tables to make this method idempotent + self.command_group_table.clear() + self.command_table.clear() + + command_index = None + # Set fallback=False to turn off command index in case of regression + use_command_index = self.cli_ctx.config.getboolean('core', 'use_command_index', fallback=True) + if use_command_index: + command_index = CommandIndex(self.cli_ctx) + index_result = command_index.get(args) + if index_result: + index_modules, index_extensions = index_result + # Always load modules and extensions, because some of them (like those in + # ALWAYS_LOADED_EXTENSIONS) don't expose a command, but hooks into handlers in CLI core + _update_command_table_from_modules(args, index_modules) + # The index won't contain suppressed extensions + _update_command_table_from_extensions([], index_extensions) + + logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table)) + # The index may be outdated. Make sure the command appears in the loaded command table + command_str = _roughly_parse_command(args) + if command_str in self.command_table: + logger.debug("Found a match in the command table for '%s'", command_str) + return self.command_table + if command_str in self.command_group_table: + logger.debug("Found a match in the command group table for '%s'", command_str) + return self.command_table + + logger.debug("Could not find a match in the command table for '%s'. The index may be outdated", + command_str) + else: + logger.debug("No module found from index for '%s'", args) + + # No module found from the index. Load all command modules and extensions + logger.debug("Loading all modules and extensions") _update_command_table_from_modules(args) - try: - ext_suppressions = _get_extension_suppressions(self.loaders) - # We always load extensions even if the appropriate module has been loaded - # as an extension could override the commands already loaded. - _update_command_table_from_extensions(ext_suppressions) - except Exception: # pylint: disable=broad-except - logger.warning("Unable to load extensions. Use --debug for more information.") - logger.debug(traceback.format_exc()) + + ext_suppressions = _get_extension_suppressions(self.loaders) + # We always load extensions even if the appropriate module has been loaded + # as an extension could override the commands already loaded. + _update_command_table_from_extensions(ext_suppressions) + logger.debug("Loaded %d groups, %d commands.", len(self.command_group_table), len(self.command_table)) + + if use_command_index: + command_index.update(self.command_table) return self.command_table @@ -348,6 +474,111 @@ loader._update_command_definitions() # pylint: disable=protected-access +class CommandIndex: + + _COMMAND_INDEX = 'commandIndex' + _COMMAND_INDEX_VERSION = 'version' + _COMMAND_INDEX_CLOUD_PROFILE = 'cloudProfile' + + def __init__(self, cli_ctx=None): + """Class to manage command index. + + :param cli_ctx: Only needed when `get` or `update` is called. + """ + from azure.cli.core._session import INDEX + self.INDEX = INDEX + if cli_ctx: + self.version = __version__ + self.cloud_profile = cli_ctx.cloud.profile + + def get(self, args): + """Get the corresponding module and extension list of a command. + + :param args: command arguments, like ['network', 'vnet', 'create', '-h'] + :return: a tuple containing a list of modules and a list of extensions. + """ + # If the command index version or cloud profile doesn't match those of the current command, + # invalidate the command index. + index_version = self.INDEX[self._COMMAND_INDEX_VERSION] + cloud_profile = self.INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] + if not (index_version and index_version == self.version and + cloud_profile and cloud_profile == self.cloud_profile): + logger.debug("Command index version or cloud profile is invalid or doesn't match the current command.") + self.invalidate() + return None + + # Make sure the top-level command is provided, like `az version`. + # Skip command index for `az` or `az --help`. + if not args or args[0].startswith('-'): + return None + + # Get the top-level command, like `network` in `network vnet create -h` + top_command = args[0] + index = self.INDEX[self._COMMAND_INDEX] + # Check the command index for (command: [module]) mapping, like + # "network": ["azure.cli.command_modules.natgateway", "azure.cli.command_modules.network", "azext_firewall"] + index_modules_extensions = index.get(top_command) + + if index_modules_extensions: + # This list contains both built-in modules and extensions + index_builtin_modules = [] + index_extensions = [] + # Found modules from index + logger.debug("Modules found from index for '%s': %s", top_command, index_modules_extensions) + command_module_prefix = 'azure.cli.command_modules.' + for m in index_modules_extensions: + if m.startswith(command_module_prefix): + # The top-level command is from a command module + index_builtin_modules.append(m[len(command_module_prefix):]) + elif m.startswith('azext_'): + # The top-level command is from an extension + index_extensions.append(m) + else: + logger.warning("Unrecognized module: %s", m) + return index_builtin_modules, index_extensions + + return None + + def update(self, command_table): + """Update the command index according to the given command table. + + :param command_table: The command table built by azure.cli.core.MainCommandsLoader.load_command_table + """ + start_time = timeit.default_timer() + self.INDEX[self._COMMAND_INDEX_VERSION] = __version__ + self.INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] = self.cloud_profile + from collections import defaultdict + index = defaultdict(list) + + # self.cli_ctx.invocation.commands_loader.command_table doesn't exist in DummyCli due to the lack of invocation + for command_name, command in command_table.items(): + # Get the top-level name: <vm> create + top_command = command_name.split()[0] + # Get module name, like azure.cli.command_modules.vm, azext_webapp + module_name = command.loader.__module__ + if module_name not in index[top_command]: + index[top_command].append(module_name) + elapsed_time = timeit.default_timer() - start_time + self.INDEX[self._COMMAND_INDEX] = index + logger.debug("Updated command index in %.3f seconds.", elapsed_time) + + def invalidate(self): + """Invalidate the command index. + + This function MUST be called when installing or updating extensions. Otherwise, when an extension + 1. overrides a built-in command, or + 2. extends an existing command group, + the command or command group will only be loaded from the command modules as per the stale command index, + making the newly installed extension be ignored. + + This function can be called when removing extensions. + """ + self.INDEX[self._COMMAND_INDEX_VERSION] = "" + self.INDEX[self._COMMAND_INDEX_CLOUD_PROFILE] = "" + self.INDEX[self._COMMAND_INDEX] = {} + logger.debug("Command index has been invalidated.") + + class ModExtensionSuppress(object): # pylint: disable=too-few-public-methods def __init__(self, mod_name, suppress_extension_name, suppress_up_to_version, reason=None, recommend_remove=False, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/_profile.py new/azure-cli-core-2.9.1/azure/cli/core/_profile.py --- old/azure-cli-core-2.7.0/azure/cli/core/_profile.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/_profile.py 2020-07-16 10:09:55.000000000 +0200 @@ -14,7 +14,6 @@ import string from copy import deepcopy from enum import Enum -from six.moves import BaseHTTPServer from azure.cli.core._environment import get_config_dir from azure.cli.core._session import ACCOUNT @@ -307,18 +306,18 @@ import jwt from requests import HTTPError - from msrestazure.azure_active_directory import MSIAuthentication from msrestazure.tools import is_valid_resource_id + from azure.cli.core.adal_authentication import MSIAuthenticationWrapper resource = self.cli_ctx.cloud.endpoints.active_directory_resource_id if identity_id: if is_valid_resource_id(identity_id): - msi_creds = MSIAuthentication(resource=resource, msi_res_id=identity_id) + msi_creds = MSIAuthenticationWrapper(resource=resource, msi_res_id=identity_id) identity_type = MsiAccountTypes.user_assigned_resource_id else: authenticated = False try: - msi_creds = MSIAuthentication(resource=resource, client_id=identity_id) + msi_creds = MSIAuthenticationWrapper(resource=resource, client_id=identity_id) identity_type = MsiAccountTypes.user_assigned_client_id authenticated = True except HTTPError as ex: @@ -330,7 +329,7 @@ if not authenticated: try: identity_type = MsiAccountTypes.user_assigned_object_id - msi_creds = MSIAuthentication(resource=resource, object_id=identity_id) + msi_creds = MSIAuthenticationWrapper(resource=resource, object_id=identity_id) authenticated = True except HTTPError as ex: if ex.response.reason == 'Bad Request' and ex.response.status == 400: @@ -343,7 +342,7 @@ else: identity_type = MsiAccountTypes.system_assigned - msi_creds = MSIAuthentication(resource=resource) + msi_creds = MSIAuthenticationWrapper(resource=resource) token_entry = msi_creds.token token = token_entry['access_token'] @@ -388,8 +387,8 @@ return deepcopy(consolidated) def _get_token_from_cloud_shell(self, resource): # pylint: disable=no-self-use - from msrestazure.azure_active_directory import MSIAuthentication - auth = MSIAuthentication(resource=resource) + from azure.cli.core.adal_authentication import MSIAuthenticationWrapper + auth = MSIAuthenticationWrapper(resource=resource) auth.set_token() token_entry = auth.token return (token_entry['token_type'], token_entry['access_token'], token_entry) @@ -650,9 +649,11 @@ tenant_dest, resource) else: # Service Principal + use_cert_sn_issuer = bool(account[_USER_ENTITY].get(_SERVICE_PRINCIPAL_CERT_SN_ISSUER_AUTH)) creds = self._creds_cache.retrieve_token_for_service_principal(username_or_sp_id, resource, - tenant_dest) + tenant_dest, + use_cert_sn_issuer) return (creds, None if tenant else str(account[_SUBSCRIPTION_ID]), str(tenant if tenant else account[_TENANT_ID])) @@ -772,15 +773,15 @@ @staticmethod def msi_auth_factory(cli_account_name, identity, resource): - from msrestazure.azure_active_directory import MSIAuthentication + from azure.cli.core.adal_authentication import MSIAuthenticationWrapper if cli_account_name == MsiAccountTypes.system_assigned: - return MSIAuthentication(resource=resource) + return MSIAuthenticationWrapper(resource=resource) if cli_account_name == MsiAccountTypes.user_assigned_client_id: - return MSIAuthentication(resource=resource, client_id=identity) + return MSIAuthenticationWrapper(resource=resource, client_id=identity) if cli_account_name == MsiAccountTypes.user_assigned_object_id: - return MSIAuthentication(resource=resource, object_id=identity) + return MSIAuthenticationWrapper(resource=resource, object_id=identity) if cli_account_name == MsiAccountTypes.user_assigned_resource_id: - return MSIAuthentication(resource=resource, msi_res_id=identity) + return MSIAuthenticationWrapper(resource=resource, msi_res_id=identity) raise ValueError("unrecognized msi account name '{}'".format(cli_account_name)) @@ -1132,22 +1133,25 @@ def __init__(self, password_arg_value, use_cert_sn_issuer=None): if not password_arg_value: raise CLIError('missing secret or certificate in order to ' - 'authnenticate through a service principal') + 'authenticate through a service principal') if os.path.isfile(password_arg_value): certificate_file = password_arg_value from OpenSSL.crypto import load_certificate, FILETYPE_PEM self.certificate_file = certificate_file self.public_certificate = None - with open(certificate_file, 'r') as file_reader: - self.cert_file_string = file_reader.read() - cert = load_certificate(FILETYPE_PEM, self.cert_file_string) - self.thumbprint = cert.digest("sha1").decode() - if use_cert_sn_issuer: - # low-tech but safe parsing based on - # https://github.com/libressl-portable/openbsd/blob/master/src/lib/libcrypto/pem/pem.h - match = re.search(r'\-+BEGIN CERTIFICATE.+\-+(?P<public>[^-]+)\-+END CERTIFICATE.+\-+', - self.cert_file_string, re.I) - self.public_certificate = match.group('public').strip() + try: + with open(certificate_file, 'r') as file_reader: + self.cert_file_string = file_reader.read() + cert = load_certificate(FILETYPE_PEM, self.cert_file_string) + self.thumbprint = cert.digest("sha1").decode() + if use_cert_sn_issuer: + # low-tech but safe parsing based on + # https://github.com/libressl-portable/openbsd/blob/master/src/lib/libcrypto/pem/pem.h + match = re.search(r'\-+BEGIN CERTIFICATE.+\-+(?P<public>[^-]+)\-+END CERTIFICATE.+\-+', + self.cert_file_string, re.I) + self.public_certificate = match.group('public').strip() + except UnicodeDecodeError: + raise CLIError('Invalid certificate, please use a valid PEM file.') else: self.secret = password_arg_value @@ -1171,43 +1175,43 @@ return entry -class ClientRedirectServer(BaseHTTPServer.HTTPServer): # pylint: disable=too-few-public-methods - query_params = {} - +def _get_authorization_code_worker(authority_url, resource, results): + # pylint: disable=too-many-statements + import socket + import random + import http.server -class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): - # pylint: disable=line-too-long + class ClientRedirectServer(http.server.HTTPServer): # pylint: disable=too-few-public-methods + query_params = {} - def do_GET(self): - try: - from urllib.parse import parse_qs - except ImportError: - from urlparse import parse_qs # pylint: disable=import-error - - if self.path.endswith('/favicon.ico'): # deal with legacy IE - self.send_response(204) - return - - query = self.path.split('?', 1)[-1] - query = parse_qs(query, keep_blank_values=True) - self.server.query_params = query - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - - landing_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'auth_landing_pages', - 'ok.html' if 'code' in query else 'fail.html') - with open(landing_file, 'rb') as html_file: - self.wfile.write(html_file.read()) - - def log_message(self, format, *args): # pylint: disable=redefined-builtin,unused-argument,no-self-use - pass # this prevent http server from dumping messages to stdout + class ClientRedirectHandler(http.server.BaseHTTPRequestHandler): + # pylint: disable=line-too-long + def do_GET(self): + try: + from urllib.parse import parse_qs + except ImportError: + from urlparse import parse_qs # pylint: disable=import-error + + if self.path.endswith('/favicon.ico'): # deal with legacy IE + self.send_response(204) + return + + query = self.path.split('?', 1)[-1] + query = parse_qs(query, keep_blank_values=True) + self.server.query_params = query + + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + + landing_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'auth_landing_pages', + 'ok.html' if 'code' in query else 'fail.html') + with open(landing_file, 'rb') as html_file: + self.wfile.write(html_file.read()) -def _get_authorization_code_worker(authority_url, resource, results): - import socket - import random + def log_message(self, format, *args): # pylint: disable=redefined-builtin,unused-argument,no-self-use + pass # this prevent http server from dumping messages to stdout reply_url = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/_session.py new/azure-cli-core-2.9.1/azure/cli/core/_session.py --- old/azure-cli-core-2.7.0/azure/cli/core/_session.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/_session.py 2020-07-16 10:09:55.000000000 +0200 @@ -105,6 +105,9 @@ # SESSION provides read-write session variables SESSION = Session() +# INDEX contains {top-level command: [command_modules and extensions]} mapping index +INDEX = Session() + # VERSIONS provides local versions and pypi versions. # DO NOT USE it to get the current version of azure-cli, # it could be lagged behind and can be used to check whether diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/adal_authentication.py new/azure-cli-core-2.9.1/azure/cli/core/adal_authentication.py --- old/azure-cli-core-2.7.0/azure/cli/core/adal_authentication.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/adal_authentication.py 2020-07-16 10:09:55.000000000 +0200 @@ -8,6 +8,7 @@ import adal from msrest.authentication import Authentication +from msrestazure.azure_active_directory import MSIAuthentication from azure.core.credentials import AccessToken from azure.cli.core.util import in_cloud_console @@ -60,8 +61,10 @@ # This method is exposed for Azure Core. def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument _, token, full_token, _ = self._get_token() - - return AccessToken(token, int(full_token['expiresIn'] + time.time())) + try: + return AccessToken(token, int(full_token['expiresIn'] + time.time())) + except KeyError: # needed to deal with differing unserialized MSI token payload + return AccessToken(token, int(full_token['expires_on'])) # This method is exposed for msrest. def signed_session(self, session=None): # pylint: disable=arguments-differ @@ -83,3 +86,10 @@ logger = get_logger(__name__) logger.warning("A Cloud Shell credential problem occurred. When you report the issue with the error " "below, please mention the hostname '%s'", socket.gethostname()) + + +class MSIAuthenticationWrapper(MSIAuthentication): + # This method is exposed for Azure Core. + def get_token(self): + self.set_token() + return AccessToken(self.token['access_token'], int(self.token['expires_on'])) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/cloud.py new/azure-cli-core-2.9.1/azure/cli/core/cloud.py --- old/azure-cli-core-2.7.0/azure/cli/core/cloud.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/cloud.py 2020-07-16 10:09:55.000000000 +0200 @@ -114,6 +114,7 @@ def __init__(self, storage_endpoint=None, + storage_sync_endpoint=None, keyvault_dns=None, sql_server_hostname=None, azure_datalake_store_file_system_endpoint=None, @@ -124,6 +125,7 @@ mariadb_server_endpoint=None): # Attribute names are significant. They are used when storing/retrieving clouds from config self.storage_endpoint = storage_endpoint + self.storage_sync_endpoint = storage_sync_endpoint self.keyvault_dns = keyvault_dns self.sql_server_hostname = sql_server_hostname self.mysql_server_endpoint = mysql_server_endpoint @@ -163,6 +165,14 @@ return graph_endpoint_mapper.get(cloud_name, None) +def _get_storage_sync_endpoint(cloud_name): + storage_sync_endpoint_mapper = { + 'AzureCloud': 'afs.azure.net', + 'AzureUSGovernment': 'afs.azure.us', + } + return storage_sync_endpoint_mapper.get(cloud_name, 'afs.azure.net') + + def _convert_arm_to_cli(arm_cloud_metadata_dict): cli_cloud_metadata_dict = {} for cloud in arm_cloud_metadata_dict: @@ -191,6 +201,7 @@ log_analytics_resource_id=arm_dict['logAnalyticsResourceId'] if 'logAnalyticsResourceId' in arm_dict else None), # pylint: disable=line-too-long suffixes=CloudSuffixes( storage_endpoint=arm_dict['suffixes']['storage'], + storage_sync_endpoint=arm_dict['suffix']['storageSyncEndpointSuffix'] if 'storageSyncEndpointSuffix' in arm_dict['suffixes'] else _get_storage_sync_endpoint(arm_dict['name']), # pylint: disable=line-too-long keyvault_dns=arm_dict['suffixes']['keyVaultDns'], sql_server_hostname=arm_dict['suffixes']['sqlServerHostname'], mysql_server_endpoint=arm_dict['suffixes']['mysqlServerEndpoint'], @@ -248,6 +259,7 @@ app_insights_telemetry_channel_resource_id='https://dc.applicationinsights.azure.com/v2/track'), suffixes=CloudSuffixes( storage_endpoint='core.windows.net', + storage_sync_endpoint='afs.azure.net', keyvault_dns='.vault.azure.net', sql_server_hostname='.database.windows.net', mysql_server_endpoint='.mysql.database.azure.com', @@ -304,6 +316,7 @@ app_insights_telemetry_channel_resource_id='https://dc.applicationinsights.us/v2/track'), suffixes=CloudSuffixes( storage_endpoint='core.usgovcloudapi.net', + storage_sync_endpoint='afs.azure.us', keyvault_dns='.vault.usgovcloudapi.net', sql_server_hostname='.database.usgovcloudapi.net', mysql_server_endpoint='.mysql.database.usgovcloudapi.net', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/commands/__init__.py new/azure-cli-core-2.9.1/azure/cli/core/commands/__init__.py --- old/azure-cli-core-2.7.0/azure/cli/core/commands/__init__.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/commands/__init__.py 2020-07-16 10:09:55.000000000 +0200 @@ -21,7 +21,7 @@ # pylint: disable=unused-import from azure.cli.core.commands.constants import ( - BLACKLISTED_MODS, DEFAULT_QUERY_TIME_RANGE, CLI_COMMON_KWARGS, CLI_COMMAND_KWARGS, CLI_PARAM_KWARGS, + BLOCKED_MODS, DEFAULT_QUERY_TIME_RANGE, CLI_COMMON_KWARGS, CLI_COMMAND_KWARGS, CLI_PARAM_KWARGS, CLI_POSITIONAL_PARAM_KWARGS, CONFIRM_PARAM_NAME) from azure.cli.core.commands.parameters import ( AzArgumentContext, patch_arg_make_required, patch_arg_make_optional) @@ -827,11 +827,13 @@ @staticmethod def remove_additional_prop_layer(obj, converted_dic): - from msrest.serialization import Model - if isinstance(obj, Model): - # let us make sure this is the additional properties auto-generated by SDK - if ('additionalProperties' in converted_dic and isinstance(obj.additional_properties, dict)): + # Follow EAFP to flatten `additional_properties` auto-generated by SDK + # See https://docs.python.org/3/glossary.html#term-eafp + try: + if 'additionalProperties' in converted_dic and isinstance(obj.additional_properties, dict): converted_dic.update(converted_dic.pop('additionalProperties')) + except AttributeError: + pass return converted_dic def _validate_cmd_level(self, ns, cmd_validator): # pylint: disable=no-self-use @@ -843,12 +845,14 @@ pass def _validate_arg_level(self, ns, **_): # pylint: disable=no-self-use - from msrest.exceptions import ValidationError for validator in getattr(ns, '_argument_validators', []): try: validator(**self._build_kwargs(validator, ns)) - except ValidationError: - logger.debug('Validation error in %s.', str(validator)) + except Exception as ex: + # Delay the import and mimic an exception handler + from msrest.exceptions import ValidationError + if isinstance(ex, ValidationError): + logger.debug('Validation error in %s.', str(validator)) raise try: delattr(ns, '_argument_validators') @@ -1010,6 +1014,13 @@ def _load_command_loader(loader, args, name, prefix): module = import_module(prefix + name) loader_cls = getattr(module, 'COMMAND_LOADER_CLS', None) + if not loader_cls: + try: + get_command_loader = getattr(module, 'get_command_loader', None) + loader_cls = get_command_loader(loader.cli_ctx) + except (ImportError, AttributeError, TypeError): + logger.debug("Module '%s' is missing `get_command_loader` entry.", name) + command_table = {} if loader_cls: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/commands/arm.py new/azure-cli-core-2.9.1/azure/cli/core/commands/arm.py --- old/azure-cli-core-2.7.0/azure/cli/core/commands/arm.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/commands/arm.py 2020-07-16 10:09:55.000000000 +0200 @@ -179,7 +179,6 @@ def register_ids_argument(cli_ctx): from knack import events - from msrestazure.tools import parse_resource_id, is_valid_resource_id ids_metadata = {} @@ -305,6 +304,7 @@ if full_id_list: setattr(namespace, '_ids', full_id_list) + from azure.mgmt.core.tools import parse_resource_id, is_valid_resource_id for val in full_id_list: if not is_valid_resource_id(val): raise CLIError('invalid resource ID: {}'.format(val)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/commands/constants.py new/azure-cli-core-2.9.1/azure/cli/core/commands/constants.py --- old/azure-cli-core-2.7.0/azure/cli/core/commands/constants.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/commands/constants.py 2020-07-16 10:09:55.000000000 +0200 @@ -30,11 +30,11 @@ # 1 hour in milliseconds DEFAULT_QUERY_TIME_RANGE = 3600000 -BLACKLISTED_MODS = ['context', 'shell', 'documentdb', 'component'] +BLOCKED_MODS = ['context', 'shell', 'documentdb', 'component'] -SURVEY_PROMPT = 'Please let us know how we are doing: https://aka.ms/clihats' +SURVEY_PROMPT = 'Please let us know how we are doing: https://aka.ms/azureclihats' SURVEY_PROMPT_COLOR = Fore.YELLOW + Style.BRIGHT + 'Please let us know how we are doing: ' + Fore.BLUE + \ - 'https://aka.ms/clihats' + Style.RESET_ALL + 'https://aka.ms/azureclihats' + Style.RESET_ALL UX_SURVEY_PROMPT = 'and let us know if you\'re interested in trying out our newest features: https://aka.ms/CLIUXstudy' UX_SURVEY_PROMPT_COLOR = Fore.YELLOW + Style.BRIGHT + \ 'and let us know if you\'re interested in trying out our newest features: ' \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/decorators.py new/azure-cli-core-2.9.1/azure/cli/core/decorators.py --- old/azure-cli-core-2.7.0/azure/cli/core/decorators.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/decorators.py 2020-07-16 10:09:55.000000000 +0200 @@ -72,8 +72,9 @@ def _wrapped_func(*args, **kwargs): try: return func(*args, **kwargs) - except Exception as ex: # nopa pylint: disable=broad-except - get_logger(__name__).info('Suppress exception %s', ex) + except Exception: # nopa pylint: disable=broad-except + import traceback + get_logger(__name__).info('Suppress exception:\n%s', traceback.format_exc()) if fallback_return is not None: return fallback_return return _wrapped_func diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/extension/_index.py new/azure-cli-core-2.9.1/azure/cli/core/extension/_index.py --- old/azure-cli-core-2.7.0/azure/cli/core/extension/_index.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/extension/_index.py 2020-07-16 10:09:55.000000000 +0200 @@ -2,7 +2,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import requests from knack.log import get_logger from knack.util import CLIError @@ -22,6 +21,7 @@ # pylint: disable=inconsistent-return-statements def get_index(index_url=None): + import requests from azure.cli.core.util import should_disable_connection_verify index_url = index_url or DEFAULT_INDEX_URL diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/extension/_resolve.py new/azure-cli-core-2.9.1/azure/cli/core/extension/_resolve.py --- old/azure-cli-core-2.7.0/azure/cli/core/extension/_resolve.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/extension/_resolve.py 2020-07-16 10:09:55.000000000 +0200 @@ -53,15 +53,19 @@ return filter_func -def resolve_from_index(extension_name, cur_version=None, index_url=None): +def resolve_from_index(extension_name, cur_version=None, index_url=None, target_version=None): """ Gets the download Url and digest for the matching extension + + :param cur_version: threshold verssion to filter out extensions. """ candidates = get_index_extensions(index_url=index_url).get(extension_name, []) + if not candidates: raise NoExtensionCandidatesError("No extension found with name '{}'".format(extension_name)) filters = [_is_not_platform_specific, _is_compatible_with_cli_version, _is_greater_than_cur_version(cur_version)] + for f in filters: logger.debug("Candidates %s", [c['filename'] for c in candidates]) candidates = list(filter(f, candidates)) @@ -71,7 +75,15 @@ candidates_sorted = sorted(candidates, key=lambda c: parse_version(c['metadata']['version']), reverse=True) logger.debug("Candidates %s", [c['filename'] for c in candidates_sorted]) logger.debug("Choosing the latest of the remaining candidates.") - chosen = candidates_sorted[0] + + if target_version: + try: + chosen = [c for c in candidates_sorted if c['metadata']['version'] == target_version][0] + except IndexError: + raise NoExtensionCandidatesError('Extension with version {} not found'.format(target_version)) + else: + chosen = candidates_sorted[0] + logger.debug("Chosen %s", chosen) download_url, digest = chosen.get('downloadUrl'), chosen.get('sha256Digest') if not download_url: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/extension/operations.py new/azure-cli-core-2.9.1/azure/cli/core/extension/operations.py --- old/azure-cli-core-2.7.0/azure/cli/core/extension/operations.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/extension/operations.py 2020-07-16 10:09:55.000000000 +0200 @@ -15,9 +15,9 @@ from subprocess import check_output, STDOUT, CalledProcessError from six.moves.urllib.parse import urlparse # pylint: disable=import-error -import requests from pkg_resources import parse_version +from azure.cli.core import CommandIndex from azure.cli.core.util import CLIError, reload_module from azure.cli.core.extension import (extension_exists, build_extension_path, get_extensions, get_extension_modname, get_extension, ext_compat_with_cli, @@ -63,6 +63,7 @@ def _whl_download_from_url(url_parse_result, ext_file): + import requests from azure.cli.core.util import should_disable_connection_verify url = url_parse_result.geturl() r = requests.get(url, stream=True, verify=(not should_disable_connection_verify())) @@ -105,6 +106,7 @@ tmp_dir = tempfile.mkdtemp() ext_file = os.path.join(tmp_dir, whl_filename) logger.debug('Downloading %s to %s', source, ext_file) + import requests try: cmd.cli_ctx.get_progress_controller().add(message='Downloading') _whl_download_from_url(url_parse_result, ext_file) @@ -188,8 +190,8 @@ def check_version_compatibility(azext_metadata): is_compatible, cli_core_version, min_required, max_required = ext_compat_with_cli(azext_metadata) - logger.debug("Extension compatibility result: is_compatible=%s cli_core_version=%s min_required=%s " - "max_required=%s", is_compatible, cli_core_version, min_required, max_required) + # logger.debug("Extension compatibility result: is_compatible=%s cli_core_version=%s min_required=%s " + # "max_required=%s", is_compatible, cli_core_version, min_required, max_required) if not is_compatible: min_max_msg_fmt = "The '{}' extension is not compatible with this version of the CLI.\n" \ "You have CLI core version {} and this extension " \ @@ -205,8 +207,12 @@ def add_extension(cmd, source=None, extension_name=None, index_url=None, yes=None, # pylint: disable=unused-argument - pip_extra_index_urls=None, pip_proxy=None, system=None): + pip_extra_index_urls=None, pip_proxy=None, system=None, + version=None): ext_sha256 = None + + version = None if version == 'latest' else version + if extension_name: cmd.cli_ctx.get_progress_controller().add(message='Searching') ext = None @@ -220,10 +226,16 @@ return logger.warning("Overriding development version of '%s' with production version.", extension_name) try: - source, ext_sha256 = resolve_from_index(extension_name, index_url=index_url) + source, ext_sha256 = resolve_from_index(extension_name, index_url=index_url, target_version=version) except NoExtensionCandidatesError as err: logger.debug(err) - raise CLIError("No matching extensions for '{}'. Use --debug for more information.".format(extension_name)) + + if version: + err = "No matching extensions for '{} ({})'. Use --debug for more information.".format(extension_name, version) + else: + err = "No matching extensions for '{}'. Use --debug for more information.".format(extension_name) + raise CLIError(err) + extension_name = _add_whl_ext(cmd=cmd, source=source, ext_sha256=ext_sha256, pip_extra_index_urls=pip_extra_index_urls, pip_proxy=pip_proxy, system=system) try: @@ -234,6 +246,7 @@ "Please use with discretion.", extension_name) elif extension_name and ext.preview: logger.warning("The installed extension '%s' is in preview.", extension_name) + CommandIndex().invalidate() except ExtensionNotInstalledException: pass @@ -253,6 +266,7 @@ # We call this just before we remove the extension so we can get the metadata before it is gone _augment_telemetry_with_ext_info(extension_name, ext) shutil.rmtree(ext.path, onerror=log_err) + CommandIndex().invalidate() except ExtensionNotInstalledException as e: raise CLIError(e) @@ -305,6 +319,7 @@ logger.debug('Copying %s to %s', backup_dir, extension_path) shutil.copytree(backup_dir, extension_path) raise CLIError('Failed to update. Rolled {} back to {}.'.format(extension_name, cur_version)) + CommandIndex().invalidate() except ExtensionNotInstalledException as e: raise CLIError(e) @@ -347,6 +362,24 @@ def add_extension_to_path(extension_name, ext_dir=None): ext_dir = ext_dir or get_extension(extension_name).path sys.path.append(ext_dir) + # If this path update should have made a new "azure" module available, + # extend the existing module with its path. This allows extensions to + # include (or depend on) Azure SDK modules that are not yet part of + # the CLI. This applies to both the "azure" and "azure.mgmt" namespaces, + # but ensures that modules installed by the CLI take priority. + azure_dir = os.path.join(ext_dir, "azure") + if os.path.isdir(azure_dir): + import azure + azure.__path__.append(azure_dir) + azure_mgmt_dir = os.path.join(azure_dir, "mgmt") + if os.path.isdir(azure_mgmt_dir): + try: + # Should have been imported already, so this will be quick + import azure.mgmt + except ImportError: + pass + else: + azure.mgmt.__path__.append(azure_mgmt_dir) def get_lsb_release(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/parser.py new/azure-cli-core-2.9.1/azure/cli/core/parser.py --- old/azure-cli-core-2.7.0/azure/cli/core/parser.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/parser.py 2020-07-16 10:09:55.000000000 +0200 @@ -152,6 +152,9 @@ with CommandLoggerContext(logger): logger.error('%(prog)s: error: %(message)s', args) self.print_usage(sys.stderr) + # Manual recommendations + self._set_manual_recommendations(args['message']) + # AI recommendations failure_recovery_recommendations = self._get_failure_recovery_recommendations() self._suggestion_msg.extend(failure_recovery_recommendations) self._print_suggestion_msg(sys.stderr) @@ -179,6 +182,14 @@ argcomplete.autocomplete(self, validator=lambda c, p: c.lower().startswith(p.lower()), default_completer=lambda _: ()) + def _set_manual_recommendations(self, error_msg): + recommendations = [] + # recommendation for --query value error + if '--query' in error_msg: + recommendations.append('To learn more about [--query JMESPATH] usage in AzureCLI, ' + 'visit https://aka.ms/CLIQuery') + self._suggestion_msg.extend(recommendations) + def _get_failure_recovery_arguments(self, action=None): # Strip the leading "az " and any extraneous whitespace. command = self.prog[3:].strip() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/profiles/_shared.py new/azure-cli-core-2.9.1/azure/cli/core/profiles/_shared.py --- old/azure-cli-core-2.7.0/azure/cli/core/profiles/_shared.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/profiles/_shared.py 2020-07-16 10:09:55.000000000 +0200 @@ -131,22 +131,23 @@ AZURE_API_PROFILES = { 'latest': { ResourceType.MGMT_STORAGE: '2019-06-01', - ResourceType.MGMT_NETWORK: '2020-04-01', - ResourceType.MGMT_COMPUTE: SDKProfile('2019-07-01', { + ResourceType.MGMT_NETWORK: '2020-05-01', + ResourceType.MGMT_COMPUTE: SDKProfile('2020-06-01', { 'resource_skus': '2019-04-01', - 'disks': '2019-11-01', + 'disks': '2020-05-01', + 'disk_encryption_sets': '2020-05-01', 'snapshots': '2019-07-01', 'galleries': '2019-12-01', 'gallery_images': '2019-12-01', 'gallery_image_versions': '2019-12-01', - 'virtual_machine_scale_sets': '2019-12-01' + 'virtual_machine_scale_sets': '2020-06-01' }), ResourceType.MGMT_RESOURCE_FEATURES: '2015-12-01', ResourceType.MGMT_RESOURCE_LINKS: '2016-09-01', ResourceType.MGMT_RESOURCE_LOCKS: '2016-09-01', ResourceType.MGMT_RESOURCE_POLICY: '2019-09-01', ResourceType.MGMT_RESOURCE_RESOURCES: '2019-07-01', - ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS: '2019-06-01', + ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS: '2019-11-01', ResourceType.MGMT_RESOURCE_DEPLOYMENTSCRIPTS: '2019-10-01-preview', ResourceType.MGMT_NETWORK_DNS: '2018-05-01', ResourceType.MGMT_KEYVAULT: '2019-09-01', @@ -163,7 +164,7 @@ ResourceType.DATA_STORAGE_FILESHARE: '2019-07-07', ResourceType.DATA_STORAGE_QUEUE: '2018-03-28', ResourceType.DATA_COSMOS_TABLE: '2017-04-17', - ResourceType.MGMT_EVENTHUB: '2017-04-01', + ResourceType.MGMT_EVENTHUB: '2018-01-01-preview', ResourceType.MGMT_MONITOR: SDKProfile('2019-06-01', { 'activity_log_alerts': '2017-04-01', 'activity_logs': '2015-04-01', @@ -193,7 +194,8 @@ 'private_link_scoped_resources': '2019-10-17-preview', 'private_link_scope_operation_status': '2019-10-17-preview', 'private_link_scopes': '2019-10-17-preview', - 'private_endpoint_connections': '2019-10-17-preview' + 'private_endpoint_connections': '2019-10-17-preview', + 'subscription_diagnostic_settings': '2017-05-01-preview' }), ResourceType.MGMT_APPSERVICE: '2019-08-01', ResourceType.MGMT_IOTHUB: '2020-03-01', @@ -230,7 +232,7 @@ # to have commands show up in the hybrid profile which happens to have the latest # API versions ResourceType.MGMT_APPSERVICE: '2018-02-01', - ResourceType.MGMT_EVENTHUB: '2017-04-01', + ResourceType.MGMT_EVENTHUB: '2018-01-01-preview', ResourceType.MGMT_IOTHUB: '2019-03-22' }, '2018-03-01-hybrid': { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/telemetry.py new/azure-cli-core-2.9.1/azure/cli/core/telemetry.py --- old/azure-cli-core-2.7.0/azure/cli/core/telemetry.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/telemetry.py 2020-07-16 10:09:55.000000000 +0200 @@ -437,6 +437,7 @@ def _get_shell_type(): + # This method is not accurate and needs improvement, for instance all shells on Windows return 'cmd'. if 'ZSH_VERSION' in os.environ: return 'zsh' if 'BASH_VERSION' in os.environ: @@ -445,6 +446,9 @@ return 'ksh' if 'WINDIR' in os.environ: return 'cmd' + from azure.cli.core.util import in_cloud_console + if in_cloud_console(): + return 'cloud-shell' return _remove_cmd_chars(_remove_symbols(os.environ.get('SHELL'))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure/cli/core/util.py new/azure-cli-core-2.9.1/azure/cli/core/util.py --- old/azure-cli-core-2.7.0/azure/cli/core/util.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure/cli/core/util.py 2020-07-16 10:09:55.000000000 +0200 @@ -17,12 +17,8 @@ import logging from six.moves.urllib.request import urlopen # pylint: disable=import-error - -from azure.common import AzureException -from azure.core.exceptions import AzureError from knack.log import get_logger from knack.util import CLIError, to_snake_case -from inspect import getfullargspec as get_arg_spec logger = get_logger(__name__) @@ -61,6 +57,8 @@ from msrestazure.azure_exceptions import CloudError from msrest.exceptions import HttpOperationError, ValidationError, ClientRequestError from azure.cli.core.azlogging import CommandLoggerContext + from azure.common import AzureException + from azure.core.exceptions import AzureError with CommandLoggerContext(logger): if isinstance(ex, JMESPathTypeError): @@ -477,6 +475,7 @@ """ IS this client a autorestv3/track2 one?. Could be refined later if necessary. """ + from inspect import getfullargspec as get_arg_spec args = get_arg_spec(client_class.__init__).args return "credential" in args @@ -726,34 +725,27 @@ result[key] = value uri_parameters = result or None + endpoints = cli_ctx.cloud.endpoints # If url is an ARM resource ID, like /subscriptions/xxx/resourcegroups/xxx?api-version=2019-07-01, # default to Azure Resource Manager. - # https://management.azure.com/ + subscriptions/xxx/resourcegroups/xxx?api-version=2019-07-01 + # https://management.azure.com + /subscriptions/xxx/resourcegroups/xxx?api-version=2019-07-01 if '://' not in url: - url = cli_ctx.cloud.endpoints.resource_manager + url.lstrip('/') + url = endpoints.resource_manager.rstrip('/') + url # Replace common tokens with real values. It is for smooth experience if users copy and paste the url from # Azure Rest API doc from azure.cli.core._profile import Profile - profile = Profile() + profile = Profile(cli_ctx=cli_ctx) if '{subscriptionId}' in url: url = url.replace('{subscriptionId}', cli_ctx.data['subscription_id'] or profile.get_subscription_id()) - token_subscription = None - _subscription_regexes = [re.compile('https://management.azure.com/subscriptions/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'), - re.compile('https://graph.windows.net/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})')] - for regex in _subscription_regexes: - match = regex.match(url) - if match: - token_subscription = match.groups()[0] - logger.debug('Retrieve token from subscription %s', token_subscription) - + # Prepare the Bearer token for `Authorization` header if not skip_authorization_header and url.lower().startswith('https://'): + # Prepare `resource` for `get_raw_token` if not resource: - endpoints = cli_ctx.cloud.endpoints - # If url starts with ARM endpoint, like https://management.azure.com/, - # use active_directory_resource_id for resource. - # This follows the same behavior as azure.cli.core.commands.client_factory._get_mgmt_service_client + # If url starts with ARM endpoint, like `https://management.azure.com/`, + # use `active_directory_resource_id` for resource, like `https://management.core.windows.net/`. + # This follows the same behavior as `azure.cli.core.commands.client_factory._get_mgmt_service_client` if url.lower().startswith(endpoints.resource_manager.rstrip('/')): resource = endpoints.active_directory_resource_id else: @@ -767,8 +759,20 @@ resource = value break if resource: - token_info, _, _ = profile.get_raw_token(resource, subscription=token_subscription) - logger.debug('Retrievd AAD token for resource: %s', resource or 'ARM') + # Prepare `subscription` for `get_raw_token` + # If this is an ARM request, try to extract subscription ID from the URL. + # But there are APIs which don't require subscription ID, like /subscriptions, /tenants + # TODO: In the future when multi-tenant subscription is supported, we won't be able to uniquely identify + # the token from subscription anymore. + token_subscription = None + if url.lower().startswith(endpoints.resource_manager.rstrip('/')): + token_subscription = _extract_subscription_id(url) + if token_subscription: + logger.debug('Retrieving token for resource %s, subscription %s', resource, token_subscription) + token_info, _, _ = profile.get_raw_token(resource, subscription=token_subscription) + else: + logger.debug('Retrieving token for resource %s', resource) + token_info, _, _ = profile.get_raw_token(resource) token_type, token, _ = token_info headers = headers or {} headers['Authorization'] = '{} {}'.format(token_type, token) @@ -801,6 +805,18 @@ return r +def _extract_subscription_id(url): + """Extract the subscription ID from an ARM request URL.""" + subscription_regex = '/subscriptions/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})' + match = re.search(subscription_regex, url, re.IGNORECASE) + if match: + subscription_id = match.groups()[0] + logger.debug('Found subscription ID %s in the URL %s', subscription_id, url) + return subscription_id + logger.debug('No subscription ID specified in the URL %s', url) + return None + + def _log_request(request): """Log a client request. Copied from msrest https://github.com/Azure/msrest-for-python/blob/3653d29fc44da408898b07c710290a83d196b777/msrest/http_logger.py#L39 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure_cli_core.egg-info/PKG-INFO new/azure-cli-core-2.9.1/azure_cli_core.egg-info/PKG-INFO --- old/azure-cli-core-2.7.0/azure_cli_core.egg-info/PKG-INFO 2020-05-29 10:06:40.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure_cli_core.egg-info/PKG-INFO 2020-07-16 10:10:07.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: azure-cli-core -Version: 2.7.0 +Version: 2.9.1 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation @@ -15,6 +15,18 @@ Release History =============== + 2.9.1 + ++++++ + * Minor fixes + + 2.9.0 + ++++++ + * Fix get_token() issue in msi login and `expiresIn` key error in cloud shell login credentials for track 2 SDK related commands (#14187) + + 2.8.0 + ++++++ + * Add get_command_loader() entry to support to load customized CommandLoader (#13763) + 2.7.0 ++++++ * Enable local context for location (#13682) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/azure_cli_core.egg-info/requires.txt new/azure-cli-core-2.9.1/azure_cli_core.egg-info/requires.txt --- old/azure-cli-core-2.7.0/azure_cli_core.egg-info/requires.txt 2020-05-29 10:06:40.000000000 +0200 +++ new/azure-cli-core-2.9.1/azure_cli_core.egg-info/requires.txt 2020-07-16 10:10:07.000000000 +0200 @@ -15,7 +15,7 @@ requests~=2.22 six~=1.12 pkginfo>=1.5.0.1 -azure-mgmt-resource==9.0.0 +azure-mgmt-resource==10.0.0 azure-mgmt-core==1.0.0 [:python_version<"3.0"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-core-2.7.0/setup.py new/azure-cli-core-2.9.1/setup.py --- old/azure-cli-core-2.7.0/setup.py 2020-05-29 10:06:31.000000000 +0200 +++ new/azure-cli-core-2.9.1/setup.py 2020-07-16 10:09:55.000000000 +0200 @@ -17,7 +17,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") cmdclass = {} -VERSION = "2.7.0" +VERSION = "2.9.1" # If we have source, validate that our version numbers match # This should prevent uploading releases with mismatched versions. try: @@ -67,7 +67,7 @@ 'requests~=2.22', 'six~=1.12', 'pkginfo>=1.5.0.1', - 'azure-mgmt-resource==9.0.0', + 'azure-mgmt-resource==10.0.0', 'azure-mgmt-core==1.0.0' ]