Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package azure-cli-core for openSUSE:Factory checked in at 2026-05-05 17:58:12 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/azure-cli-core (Old) and /work/SRC/openSUSE:Factory/.azure-cli-core.new.30200 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "azure-cli-core" Tue May 5 17:58:12 2026 rev:95 rq:1350989 version:2.86.0 Changes: -------- --- /work/SRC/openSUSE:Factory/azure-cli-core/azure-cli-core.changes 2026-04-20 16:14:09.660281103 +0200 +++ /work/SRC/openSUSE:Factory/.azure-cli-core.new.30200/azure-cli-core.changes 2026-05-05 17:58:19.627109098 +0200 @@ -1,0 +2,9 @@ +Tue May 5 11:41:23 UTC 2026 - John Paul Adrian Glaubitz <[email protected]> + +- New upstream release + + Version 2.86.0 + + For detailed information about changes see the + HISTORY.rst file provided with this package +- Update Requires from setup.py + +------------------------------------------------------------------- Old: ---- azure_cli_core-2.85.0.tar.gz New: ---- azure_cli_core-2.86.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ azure-cli-core.spec ++++++ --- /var/tmp/diff_new_pack.5C44VW/_old 2026-05-05 17:58:20.235134243 +0200 +++ /var/tmp/diff_new_pack.5C44VW/_new 2026-05-05 17:58:20.239134409 +0200 @@ -24,7 +24,7 @@ %global _sitelibdir %{%{pythons}_sitelib} Name: azure-cli-core -Version: 2.85.0 +Version: 2.86.0 Release: 0 Summary: Microsoft Azure CLI Core Module License: MIT @@ -56,7 +56,7 @@ Requires: %{pythons}-msal < 2.0.0 Requires: %{pythons}-msal >= 1.35.1 Requires: %{pythons}-msal-extensions < 2.0.0 -Requires: %{pythons}-msal-extensions >= 1.2.0 +Requires: %{pythons}-msal-extensions >= 1.3.1 Requires: %{pythons}-packaging >= 20.9 Requires: %{pythons}-pip Requires: %{pythons}-pkginfo >= 1.5.0.1 ++++++ azure_cli_core-2.85.0.tar.gz -> azure_cli_core-2.86.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/HISTORY.rst new/azure_cli_core-2.86.0/HISTORY.rst --- old/azure_cli_core-2.85.0/HISTORY.rst 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/HISTORY.rst 2026-05-01 02:40:32.000000000 +0200 @@ -3,6 +3,12 @@ Release History =============== +2.86.0 +++++++ +* Resolve CVE-2025-15467 (#33201) +* Resolve CVE-2025-69419 (#33201) +* Resolve CVE-2026-39892 (#33154) + 2.85.0 ++++++ * Resolve CVE-2026-26007 (#32879) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/PKG-INFO new/azure_cli_core-2.86.0/PKG-INFO --- old/azure_cli_core-2.85.0/PKG-INFO 2026-03-31 09:19:45.048640300 +0200 +++ new/azure_cli_core-2.86.0/PKG-INFO 2026-05-01 02:41:23.844106700 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: azure-cli-core -Version: 2.85.0 +Version: 2.86.0 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation @@ -28,7 +28,7 @@ Requires-Dist: jmespath Requires-Dist: knack~=0.11.0 Requires-Dist: microsoft-security-utilities-secret-masker~=1.0.0b4 -Requires-Dist: msal-extensions==1.2.0 +Requires-Dist: msal-extensions==1.3.1 Requires-Dist: msal[broker]==1.35.1; sys_platform == "win32" Requires-Dist: msal==1.35.1; sys_platform != "win32" Requires-Dist: packaging>=20.9 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/__init__.py new/azure_cli_core-2.86.0/azure/cli/core/__init__.py --- old/azure_cli_core-2.85.0/azure/cli/core/__init__.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/__init__.py 2026-05-01 02:40:32.000000000 +0200 @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long -__version__ = "2.85.0" +__version__ = "2.86.0" import os import sys diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/_help.py new/azure_cli_core-2.86.0/azure/cli/core/_help.py --- old/azure_cli_core-2.85.0/azure/cli/core/_help.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/_help.py 2026-05-01 02:40:32.000000000 +0200 @@ -129,7 +129,6 @@ def _print_detailed_help(self, cli_name, help_file): CLIPrintMixin._print_extensions_msg(help_file) super()._print_detailed_help(cli_name, help_file) - self._print_az_find_message(help_file.command) @staticmethod def _get_choices_defaults_sources_str(p): @@ -155,12 +154,6 @@ print('') @staticmethod - def _print_az_find_message(command): - indent = 0 - message = 'To search AI knowledge base for examples, use: az find "az {}"'.format(command) - _print_indent(message + '\n', indent) - - @staticmethod def _process_value_sources(p): commands, strings, urls = [], [], [] @@ -401,10 +394,7 @@ self._print_cached_help_section(groups_items, "Subgroups:", max_line_len) self._print_cached_help_section(commands_items, "Commands:", max_line_len) - - # Use same az find message as non-cached path - print() # Blank line before the message - self._print_az_find_message('') + print() from azure.cli.core.util import show_updates_available show_updates_available(new_line_after=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/_profile.py new/azure_cli_core-2.86.0/azure/cli/core/_profile.py --- old/azure_cli_core-2.85.0/azure/cli/core/_profile.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/_profile.py 2026-05-01 02:40:32.000000000 +0200 @@ -152,7 +152,9 @@ allow_no_subscriptions=False, use_cert_sn_issuer=None, show_progress=False, - claims_challenge=None): + claims_challenge=None, + skip_subscription_discovery=False, + subscription=None): """ For service principal, `password` is a dict returned by ServicePrincipalAuth.build_credential """ @@ -198,15 +200,31 @@ else: credential = identity.get_service_principal_credential(username) - if tenant: + is_bare_mode = skip_subscription_discovery and not subscription + + if skip_subscription_discovery and subscription: + # Fast path: fetch only the specified subscription (1 API call) + subscriptions = subscription_finder.find_specific_subscriptions( + tenant, credential, [subscription]) + elif is_bare_mode: + # Bare mode: no ARM subscription calls. Tenant-level account will be created below + subscriptions = [] + subscription_finder.tenants.append(tenant) + elif tenant: subscriptions = subscription_finder.find_using_specific_tenant(tenant, credential) else: subscriptions = subscription_finder.find_using_common_tenant(username, credential) if not subscriptions and not allow_no_subscriptions: - raise CLIError("No subscriptions found for {}.".format(username)) + if skip_subscription_discovery and subscription: + raise CLIError( + "The subscription '{}' could not be retrieved for '{}'. " + "Ensure the subscription exists and that you have access to it.".format( + subscription, username)) + if not is_bare_mode: + raise CLIError("No subscriptions found for {}.".format(username)) - if allow_no_subscriptions: + if allow_no_subscriptions or is_bare_mode: t_list = [s.tenant_id for s in subscriptions] bare_tenants = [t for t in subscription_finder.tenants if t not in t_list] tenant_accounts = self._build_tenant_level_accounts(bare_tenants) @@ -216,8 +234,24 @@ consolidated = self._normalize_properties(username, subscriptions, is_service_principal, bool(use_cert_sn_issuer)) + self._set_subscriptions(consolidated, preferred_subscription=subscription) + + if subscription: + matches = [s for s in consolidated + if s[_SUBSCRIPTION_ID].lower() == subscription.lower() or + s.get(_SUBSCRIPTION_NAME, '').lower() == subscription.lower()] + if matches: + return deepcopy(matches) + if skip_subscription_discovery: + # --skip-subscription-discovery + --subscription S, but S is inaccessible. + # without --allow-no-subscriptions → already errored above + # with --allow-no-subscriptions → tenant-level account only (we reach here) + logger.warning("Subscription '%s' not found. Profile has tenant-level account only.", + subscription) + else: + raise CLIError("Subscription '{}' not found. Check the ID or name and try again." + .format(subscription)) - self._set_subscriptions(consolidated) return deepcopy(consolidated) def login_with_managed_identity(self, client_id=None, object_id=None, resource_id=None, @@ -463,7 +497,8 @@ s.state = 'Enabled' return s - def _set_subscriptions(self, new_subscriptions, merge=True, secondary_key_name=None): + def _set_subscriptions(self, new_subscriptions, merge=True, secondary_key_name=None, + preferred_subscription=None): def _get_key_name(account, secondary_key_name): return (account[_SUBSCRIPTION_ID] if secondary_key_name is None @@ -489,17 +524,26 @@ dic.update((_get_key_name(x, secondary_key_name), x) for x in new_subscriptions) subscriptions = list(dic.values()) if subscriptions: - if active_one: + new_active_one = None + + # If a preferred subscription is specified, try to use it as default + if preferred_subscription: + preferred_lower = preferred_subscription.lower() + new_active_one = next( + (x for x in new_subscriptions + if x[_SUBSCRIPTION_ID].lower() == preferred_lower or + x.get(_SUBSCRIPTION_NAME, '').lower() == preferred_lower), None) + + # Fall back to previously active subscription if still present + if not new_active_one and active_one: new_active_one = next( (x for x in new_subscriptions if _match_account(x, active_subscription_id, secondary_key_name, active_secondary_key_val)), None) - for s in subscriptions: - s[_IS_DEFAULT_SUBSCRIPTION] = False + for s in subscriptions: + s[_IS_DEFAULT_SUBSCRIPTION] = False - if not new_active_one: - new_active_one = Profile._pick_working_subscription(new_subscriptions) - else: + if not new_active_one: new_active_one = Profile._pick_working_subscription(new_subscriptions) new_active_one[_IS_DEFAULT_SUBSCRIPTION] = True @@ -507,6 +551,7 @@ set_cloud_subscription(self.cli_ctx, active_cloud.name, default_sub_id) self._storage[_SUBSCRIPTIONS] = subscriptions + return subscriptions @staticmethod def _pick_working_subscription(subscriptions): @@ -856,6 +901,23 @@ self.tenants.append(tenant) return all_subscriptions + def find_specific_subscriptions(self, tenant, credential, subscription_ids): + """Fetch specific subscriptions by ID using GET /subscriptions/{id} + instead of listing all subscriptions. + https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/get + """ + client = self._create_subscription_client(credential) + all_subscriptions = [] + for sub_id in subscription_ids: + try: + s = client.subscriptions.get(sub_id) + _attach_token_tenant(s, tenant) + all_subscriptions.append(s) + except Exception as ex: # pylint: disable=broad-except + logger.warning("Failed to retrieve subscription %s: %s", sub_id, ex) + self.tenants.append(tenant) + return all_subscriptions + def _create_subscription_client(self, credential): from azure.cli.core.profiles import ResourceType, get_api_version from azure.cli.core.profiles._shared import get_client_class diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/aaz/__init__.py new/azure_cli_core-2.86.0/azure/cli/core/aaz/__init__.py --- old/azure_cli_core-2.85.0/azure/cli/core/aaz/__init__.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/aaz/__init__.py 2026-05-01 02:40:32.000000000 +0200 @@ -21,7 +21,8 @@ AAZPaginationTokenArgFormat from ._base import has_value, AAZValuePatch, AAZUndefined from ._command import AAZCommand, AAZWaitCommand, AAZCommandGroup, \ - register_callback, register_command, register_command_group, load_aaz_command_table, link_helper + register_callback, register_command, register_command_group, load_aaz_command_table, \ + load_aaz_command_table_args_guided, link_helper from ._field_type import AAZIntType, AAZFloatType, AAZStrType, AAZBoolType, AAZDictType, AAZFreeFormDictType, \ AAZListType, AAZObjectType, AAZIdentityObjectType, AAZAnyType from ._operation import AAZHttpOperation, AAZJsonInstanceUpdateOperation, AAZGenericInstanceUpdateOperation, \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/aaz/_command.py new/azure_cli_core-2.86.0/azure/cli/core/aaz/_command.py --- old/azure_cli_core-2.85.0/azure/cli/core/aaz/_command.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/aaz/_command.py 2026-05-01 02:40:32.000000000 +0200 @@ -387,6 +387,181 @@ AAZ_PACKAGE_FULL_LOAD_ENV_NAME = 'AZURE_AAZ_FULL_LOAD' +def load_aaz_command_table_args_guided(loader, aaz_pkg_name, args): + """Args-guided AAZ command tree loader. + + Instead of importing the entire AAZ package tree (all __init__.py files which eagerly + import all command classes), this function navigates only to the relevant subtree based + on CLI args. For example, ``az monitor log-analytics workspace create --help`` only loads + the ``workspace`` sub-package and the ``_create`` module, skipping all other commands. + + This requires that AAZ ``__init__.py`` files do NOT contain wildcard imports + (``from ._create import *`` etc.) -- they should be empty (just the license header). + """ + profile_pkg = _get_profile_pkg(aaz_pkg_name, loader.cli_ctx.cloud) + + command_table = {} + command_group_table = {} + if args is None or os.environ.get(AAZ_PACKAGE_FULL_LOAD_ENV_NAME, 'False').lower() == 'true': + effective_args = None # fully load + else: + effective_args = list(args) + if profile_pkg is not None: + _load_aaz_by_pkg(loader, profile_pkg, effective_args, + command_table, command_group_table) + + for group_name, command_group in command_group_table.items(): + loader.command_group_table[group_name] = command_group + for command_name, command in command_table.items(): + loader.command_table[command_name] = command + return command_table, command_group_table + + +def _try_import_module(relative_name, package): + """Try to import a module by relative name, return None on failure.""" + try: + return importlib.import_module(relative_name, package) + except ModuleNotFoundError as ex: + # Only treat "module not found" for the requested module as a benign miss. + target_mod_name = f"{package}.{relative_name.lstrip('.')}" + if ex.name == target_mod_name: + return None + # Different module is missing; propagate so the real error surfaces. + raise + except ImportError: + logger.error("Error importing module %r from package %r", relative_name, package) + raise + + +def _register_from_module(loader, mod, command_table, command_group_table): + """Scan a module's namespace for AAZCommand/AAZCommandGroup classes and register them.""" + for value in mod.__dict__.values(): + if not isinstance(value, type): + continue + if value.__module__ != mod.__name__: # skip imported classes + continue + if issubclass(value, AAZCommandGroup) and value.AZ_NAME: + command_group_table[value.AZ_NAME] = value(cli_ctx=loader.cli_ctx) + elif issubclass(value, AAZCommand) and value.AZ_NAME: + command_table[value.AZ_NAME] = value(loader=loader) + + +def _get_pkg_children(pkg): + """List child entries of a package using pkgutil. + + Returns two sets: (file_stems, subdir_names). + - file_stems: module-like stems, e.g. {'_create', '_list', '__cmd_group'} + - subdir_names: sub-package directory names, e.g. {'namespace', 'eventhub'} + """ + import pkgutil + file_stems = set() + subdir_names = set() + + pkg_path = getattr(pkg, '__path__', None) + if not pkg_path: + return file_stems, subdir_names + + for _importer, name, ispkg in pkgutil.iter_modules(pkg_path): + if ispkg: + if not name.startswith('_'): + subdir_names.add(name) + else: + file_stems.add(name) + + return file_stems, subdir_names + + +def _load_aaz_by_pkg(loader, pkg, args, command_table, command_group_table): + """Recursively navigate the AAZ package tree guided by CLI args. + + - args is None -> full recursive load of all commands under this package. + - args is empty list -> args exhausted; load current level's commands and sub-group headers. + - args has items -> try to match first arg as a command module or sub-package, + recurse with remaining args on match. + - no match on first arg -> load current level's commands and sub-group headers. + """ + base_module = pkg.__name__ + file_stems, subdir_names = _get_pkg_children(pkg) + + if args is not None and args and not args[0].startswith('-'): + first_arg = args[0].lower().replace('-', '_') + + # First arg matches a command module (e.g. "create" -> "_create") + if f"_{first_arg}" in file_stems: + mod = _try_import_module(f"._{first_arg}", base_module) + if mod: + _register_from_module(loader, mod, command_table, command_group_table) + return + + # First arg matches a sub-package (command group) + if first_arg in subdir_names: + sub_module = f"{base_module}.{first_arg}" + mod = _try_import_module('.__cmd_group', sub_module) + if mod: + _register_from_module(loader, mod, command_table, command_group_table) + sub_pkg = _try_import_module(f'.{first_arg}', base_module) + if sub_pkg: + _load_aaz_by_pkg(loader, sub_pkg, args[1:], command_table, command_group_table) + return + + # Load __cmd_group + all command modules at this level + mod = _try_import_module('.__cmd_group', base_module) + if mod: + _register_from_module(loader, mod, command_table, command_group_table) + + for stem in file_stems: + if stem.startswith('_') and not stem.startswith('__'): + mod = _try_import_module(f'.{stem}', base_module) + if mod: + _register_from_module(loader, mod, command_table, command_group_table) + + for subdir in subdir_names: + sub_module = f"{base_module}.{subdir}" + if args is None: + # Full load -> recurse into every sub-package + sub_pkg = _try_import_module(f'.{subdir}', base_module) + if sub_pkg: + _load_aaz_by_pkg(loader, sub_pkg, None, command_table, command_group_table) + else: + # Args exhausted / not matched -> load sub-group header and the first + # command so the group is non-empty and the parser creates a subparser + # for it (required for help output). + # TODO: After optimized loading is applied to the whole CLI, revisit + # this and consider a lighter approach (e.g. parser-level fix) to + # avoid importing one command per trimmed sub-group. + mod = _try_import_module('.__cmd_group', sub_module) + if mod: + _register_from_module(loader, mod, command_table, command_group_table) + sub_pkg = _try_import_module(f'.{subdir}', base_module) + if sub_pkg: + _load_first_command(loader, sub_pkg, command_table) + + +def _load_first_command(loader, pkg, command_table): + """Load the first available command module from a package. + + This ensures the command group is non-empty so the parser creates a subparser + for it, which is required for it to appear in help output. + """ + file_stems, subdir_names = _get_pkg_children(pkg) + base_module = pkg.__name__ + + # Try to load a command module at this level first + for stem in sorted(file_stems): + if stem.startswith('_') and not stem.startswith('__'): + mod = _try_import_module(f'.{stem}', base_module) + if mod: + _register_from_module(loader, mod, command_table, {}) + return + + # No command at this level, recurse into the first sub-package + for subdir in sorted(subdir_names): + sub_pkg = _try_import_module(f'.{subdir}', base_module) + if sub_pkg: + _load_first_command(loader, sub_pkg, command_table) + return + + def load_aaz_command_table(loader, aaz_pkg_name, args): """ This function is used in AzCommandsLoader.load_command_table. It will load commands in module's aaz package. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/azclierror.py new/azure_cli_core-2.86.0/azure/cli/core/azclierror.py --- old/azure_cli_core-2.85.0/azure/cli/core/azclierror.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/azclierror.py 2026-05-01 02:40:32.000000000 +0200 @@ -33,8 +33,8 @@ self.recommendations = [] self.set_recommendation(recommendation) - # AI recommendations provided by Aladdin service, with tuple form: (recommendation, description) - self.aladdin_recommendations = [] + # example recommendations with tuple form: (recommendation, description) + self.example_recommendations = [] # exception trace for the error self.exception_trace = None @@ -50,11 +50,11 @@ elif isinstance(recommendation, list): self.recommendations.extend(recommendation) - def set_aladdin_recommendation(self, recommendations): - """ Set aladdin recommendations for the error. + def set_example_recommendation(self, recommendations): + """ Set example recommendations for the error. One item should be a tuple with the form: (recommendation, description) """ - self.aladdin_recommendations.extend(recommendations) + self.example_recommendations.extend(recommendations) def set_exception_trace(self, exception_trace): self.exception_trace = exception_trace @@ -80,9 +80,12 @@ for recommendation in self.recommendations: print(recommendation, file=sys.stderr) - if self.aladdin_recommendations: - print('\nExamples from AI knowledge base:', file=sys.stderr) - for recommendation, description in self.aladdin_recommendations: + if self.example_recommendations: + print(file=sys.stderr) + if len(self.example_recommendations) > 1: # contains help examples + print("Examples from command's help:", file=sys.stderr) + + for recommendation, description in self.example_recommendations: print_styled_text(recommendation, file=sys.stderr) print_styled_text(description, file=sys.stderr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/breaking_change.py new/azure_cli_core-2.86.0/azure/cli/core/breaking_change.py --- old/azure_cli_core-2.85.0/azure/cli/core/breaking_change.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/breaking_change.py 2026-05-01 02:40:32.000000000 +0200 @@ -15,8 +15,8 @@ logger = get_logger() -NEXT_BREAKING_CHANGE_RELEASE = '2.86.0' -NEXT_BREAKING_CHANGE_DATE = 'May 2026' +NEXT_BREAKING_CHANGE_RELEASE = '2.87.0' +NEXT_BREAKING_CHANGE_DATE = 'June 2026' DEFAULT_BREAKING_CHANGE_TAG = '[Breaking Change]' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/cloud.py new/azure_cli_core-2.86.0/azure/cli/core/cloud.py --- old/azure_cli_core-2.85.0/azure/cli/core/cloud.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/cloud.py 2026-05-01 02:40:32.000000000 +0200 @@ -23,9 +23,6 @@ # Add names of clouds that don't allow telemetry data collection here such as some air-gapped clouds. CLOUDS_FORBIDDING_TELEMETRY = ['USSec', 'USNat'] -# Add names of clouds that don't allow Aladdin requests for command recommendations here -CLOUDS_FORBIDDING_ALADDIN_REQUEST = ['USSec', 'USNat'] - class CloudNotRegisteredException(Exception): def __init__(self, cloud_name): @@ -501,7 +498,7 @@ sql_management='https://management.database.sovcloud-api.fr:8443/', batch_resource_id='https://batch.sovcloud-api.fr/', gallery='https://gallery.sovcloud-api.fr/', - active_directory='https://login.sovcloud-api.fr', + active_directory='https://login.sovcloud-identity.fr', active_directory_resource_id='https://management.sovcloud-api.fr/', active_directory_graph_resource_id='https://graph.svc.sovcloud.fr/', microsoft_graph_resource_id='https://graph.svc.sovcloud.fr', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/commandIndex.latest.json new/azure_cli_core-2.86.0/azure/cli/core/commandIndex.latest.json --- old/azure_cli_core-2.85.0/azure/cli/core/commandIndex.latest.json 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/commandIndex.latest.json 2026-05-01 02:40:32.000000000 +0200 @@ -1,5 +1,5 @@ { - "version": "2.85.0", + "version": "2.86.0", "cloudProfile": "latest", "commandIndex": { "account": [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/command_recommender.py new/azure_cli_core-2.86.0/azure/cli/core/command_recommender.py --- old/azure_cli_core-2.85.0/azure/cli/core/command_recommender.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/command_recommender.py 2026-05-01 02:40:32.000000000 +0200 @@ -3,8 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from enum import Enum - from azure.cli.core import telemetry from knack.log import get_logger @@ -12,80 +10,9 @@ logger = get_logger(__name__) -class AladdinUserFaultType(Enum): - """Define the userfault types required by aladdin service - to get the command recommendations""" - - ExpectedArgument = 'ExpectedArgument' - UnrecognizedArguments = 'UnrecognizedArguments' - ValidationError = 'ValidationError' - UnknownSubcommand = 'UnknownSubcommand' - MissingRequiredParameters = 'MissingRequiredParameters' - MissingRequiredSubcommand = 'MissingRequiredSubcommand' - StorageAccountNotFound = 'StorageAccountNotFound' - Unknown = 'Unknown' - InvalidJMESPathQuery = 'InvalidJMESPathQuery' - InvalidOutputType = 'InvalidOutputType' - InvalidParameterValue = 'InvalidParameterValue' - UnableToParseCommandInput = 'UnableToParseCommandInput' - ResourceGroupNotFound = 'ResourceGroupNotFound' - InvalidDateTimeArgumentValue = 'InvalidDateTimeArgumentValue' - InvalidResourceGroupName = 'InvalidResourceGroupName' - AzureResourceNotFound = 'AzureResourceNotFound' - InvalidAccountName = 'InvalidAccountName' - - -def get_error_type(error_msg): - """The the error type of the failed command from the error message. - The error types are only consumed by aladdin service for better recommendations. - """ - - error_type = AladdinUserFaultType.Unknown - if not error_msg: - return error_type.value - - error_msg = error_msg.lower() - if 'unrecognized' in error_msg: - error_type = AladdinUserFaultType.UnrecognizedArguments - elif 'expected one argument' in error_msg or 'expected at least one argument' in error_msg \ - or 'value required' in error_msg: - error_type = AladdinUserFaultType.ExpectedArgument - elif 'misspelled' in error_msg: - error_type = AladdinUserFaultType.UnknownSubcommand - elif 'arguments are required' in error_msg or 'argument required' in error_msg: - error_type = AladdinUserFaultType.MissingRequiredParameters - if '_subcommand' in error_msg: - error_type = AladdinUserFaultType.MissingRequiredSubcommand - elif '_command_package' in error_msg: - error_type = AladdinUserFaultType.UnableToParseCommandInput - elif 'not found' in error_msg or 'could not be found' in error_msg \ - or 'resource not found' in error_msg: - error_type = AladdinUserFaultType.AzureResourceNotFound - if 'storage_account' in error_msg or 'storage account' in error_msg: - error_type = AladdinUserFaultType.StorageAccountNotFound - elif 'resource_group' in error_msg or 'resource group' in error_msg: - error_type = AladdinUserFaultType.ResourceGroupNotFound - elif 'pattern' in error_msg or 'is not a valid value' in error_msg or 'invalid' in error_msg: - error_type = AladdinUserFaultType.InvalidParameterValue - if 'jmespath_type' in error_msg: - error_type = AladdinUserFaultType.InvalidJMESPathQuery - elif 'datetime_type' in error_msg: - error_type = AladdinUserFaultType.InvalidDateTimeArgumentValue - elif '--output' in error_msg: - error_type = AladdinUserFaultType.InvalidOutputType - elif 'resource_group' in error_msg: - error_type = AladdinUserFaultType.InvalidResourceGroupName - elif 'storage_account' in error_msg: - error_type = AladdinUserFaultType.InvalidAccountName - elif "validation error" in error_msg: - error_type = AladdinUserFaultType.ValidationError - - return error_type.value - - class CommandRecommender: # pylint: disable=too-few-public-methods """Recommend a command for user when user's command fails. - It combines Aladdin recommendations and examples in help files.""" + It uses examples from help files to provide recommendations.""" def __init__(self, command, parameters, extension, error_msg, cli_ctx): """ @@ -107,8 +34,6 @@ self.cli_ctx = cli_ctx # the item is a dict with the form {'command': #, 'description': #} self.help_examples = [] - # the item is a dict with the form {'command': #, 'description': #, 'link': #} - self.aladdin_recommendations = [] def set_help_examples(self, examples): """Set help examples. @@ -119,89 +44,10 @@ self.help_examples.extend(examples) - def _set_aladdin_recommendations(self): # pylint: disable=too-many-locals - """Set Aladdin recommendations. - Call the API, parse the response and set aladdin_recommendations. - """ - - import hashlib - import json - import requests - from requests import RequestException - from http import HTTPStatus - from azure.cli.core import __version__ as version - - api_url = 'https://app.aladdin.microsoft.com/api/v1.0/suggestions' - correlation_id = telemetry._session.correlation_id # pylint: disable=protected-access - subscription_id = telemetry._get_azure_subscription_id() # pylint: disable=protected-access - event_id = telemetry._session.event_id # pylint: disable=protected-access - # Used for DDOS protection and rate limiting - user_id = telemetry._get_user_azure_id() # pylint: disable=protected-access - hashed_user_id = hashlib.sha256(user_id.encode('utf-8')).hexdigest() - - headers = { - 'Content-Type': 'application/json', - 'X-UserId': hashed_user_id - } - context = { - 'versionNumber': version, - 'errorType': get_error_type(self.error_msg) - } - - if telemetry.is_telemetry_enabled(): - if correlation_id: - context['correlationId'] = correlation_id - if subscription_id: - context['subscriptionId'] = subscription_id - if event_id: - context['eventId'] = event_id - - parameters = self._normalize_parameters(self.parameters) - parameters = [item for item in set(parameters) if item not in ['--debug', '--verbose', '--only-show-errors']] - query = { - "command": self.command, - "parameters": ','.join(parameters) - } - - response = None - try: - response = requests.get( - api_url, - params={ - 'query': json.dumps(query), - 'clientType': 'AzureCli', - 'context': json.dumps(context) - }, - headers=headers, - timeout=1) - telemetry.set_debug_info('AladdinResponseTime', response.elapsed.total_seconds()) - - except RequestException as ex: - logger.debug('Recommendation requests.get() exception: %s', ex) - telemetry.set_debug_info('AladdinException', ex.__class__.__name__) - - recommendations = [] - if response and response.status_code == HTTPStatus.OK: - for result in response.json(): - # parse the response to get the raw command - raw_command = 'az {} '.format(result['command']) - for parameter, placeholder in zip(result['parameters'].split(','), result['placeholders'].split('♠')): - raw_command += '{} {}{}'.format(parameter, placeholder, ' ' if placeholder else '') - - # format the recommendation - recommendation = { - 'command': raw_command.strip(), - 'description': result['description'], - 'link': result['link'] - } - recommendations.append(recommendation) - - self.aladdin_recommendations.extend(recommendations) - def provide_recommendations(self): """Provide recommendations when a command fails. - The recommendations are either from Aladdin service or CLI help examples, + The recommendations are from CLI help examples, which include both commands and reference links along with their descriptions. :return: The decorated recommendations @@ -273,14 +119,7 @@ if self.cli_ctx and self.cli_ctx.config.get('core', 'error_recommendation', 'on').upper() == 'OFF': return [] - # get recommendations from Aladdin service - if not self._disable_aladdin_service(): - self._set_aladdin_recommendations() - - # recommendations are either all from Aladdin or all from help examples - recommendations = self.aladdin_recommendations - if not recommendations: - recommendations = self.help_examples + recommendations = self.help_examples # sort the recommendations by parameter matching, get the top 3 recommended commands recommendations = sort_recommendations(recommendations)[:3] @@ -305,8 +144,6 @@ # add reference link as a recommendation decorated_link = [(Style.HYPERLINK, OVERVIEW_REFERENCE)] - if self.aladdin_recommendations: - decorated_link = [(Style.HYPERLINK, self.aladdin_recommendations[0]['link'])] decorated_description = [(Style.SECONDARY, 'Read more about the command in reference docs')] decorated_recommendations.append((decorated_link, decorated_description)) @@ -319,49 +156,11 @@ def _set_recommended_command_to_telemetry(self, raw_commands): """Set the recommended commands to Telemetry - Aladdin recommended commands and commands from CLI help examples are - set to different properties in Telemetry. - :param raw_commands: The recommended raw commands :type raw_commands: list """ - if self.aladdin_recommendations: - telemetry.set_debug_info('AladdinRecommendCommand', ';'.join(raw_commands)) - else: - telemetry.set_debug_info('ExampleRecommendCommand', ';'.join(raw_commands)) - - def _disable_aladdin_service(self): - """Decide whether to disable aladdin request when a command fails. - - The possible cases to disable it are: - 1. CLI context is missing - 2. In air-gapped clouds - 3. In testing environments - 4. In autocomplete mode - - :return: whether Aladdin service need to be disabled or not - :type: bool - """ - - from azure.cli.core.cloud import CLOUDS_FORBIDDING_ALADDIN_REQUEST - - # CLI is not started well - if not self.cli_ctx or not self.cli_ctx.cloud: - return True - - # for air-gapped clouds - if self.cli_ctx.cloud.name in CLOUDS_FORBIDDING_ALADDIN_REQUEST: - return True - - # for testing environments - if self.cli_ctx.__class__.__name__ == 'DummyCli': - return True - - if self.cli_ctx.data['completer_active']: - return True - - return False + telemetry.set_debug_info('ExampleRecommendCommand', ';'.join(raw_commands)) def _normalize_parameters(self, args): """Normalize a parameter list. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/helpIndex.latest.json new/azure_cli_core-2.86.0/azure/cli/core/helpIndex.latest.json --- old/azure_cli_core-2.85.0/azure/cli/core/helpIndex.latest.json 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/helpIndex.latest.json 2026-05-01 02:40:32.000000000 +0200 @@ -1,5 +1,5 @@ { - "version": "2.85.0", + "version": "2.86.0", "cloudProfile": "latest", "helpIndex": { "groups": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure/cli/core/parser.py new/azure_cli_core-2.86.0/azure/cli/core/parser.py --- old/azure_cli_core-2.85.0/azure/cli/core/parser.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure/cli/core/parser.py 2026-05-01 02:40:32.000000000 +0200 @@ -169,7 +169,7 @@ from azure.cli.core.util import QUERY_REFERENCE az_error.set_recommendation(QUERY_REFERENCE) elif recommendations: - az_error.set_aladdin_recommendation(recommendations) + az_error.set_example_recommendation(recommendations) az_error.print_error() az_error.send_telemetry() self.exit(2) @@ -324,7 +324,7 @@ recommender.set_help_examples(self.get_examples(command_name_inferred)) recommendations = recommender.provide_recommendations() if recommendations: - az_error.set_aladdin_recommendation(recommendations) + az_error.set_example_recommendation(recommendations) # remind user to check extensions if we can not find a command to recommend if isinstance(az_error, CommandNotFoundError) \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure_cli_core.egg-info/PKG-INFO new/azure_cli_core-2.86.0/azure_cli_core.egg-info/PKG-INFO --- old/azure_cli_core-2.85.0/azure_cli_core.egg-info/PKG-INFO 2026-03-31 09:19:44.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure_cli_core.egg-info/PKG-INFO 2026-05-01 02:41:23.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: azure-cli-core -Version: 2.85.0 +Version: 2.86.0 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation @@ -28,7 +28,7 @@ Requires-Dist: jmespath Requires-Dist: knack~=0.11.0 Requires-Dist: microsoft-security-utilities-secret-masker~=1.0.0b4 -Requires-Dist: msal-extensions==1.2.0 +Requires-Dist: msal-extensions==1.3.1 Requires-Dist: msal[broker]==1.35.1; sys_platform == "win32" Requires-Dist: msal==1.35.1; sys_platform != "win32" Requires-Dist: packaging>=20.9 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/azure_cli_core.egg-info/requires.txt new/azure_cli_core-2.86.0/azure_cli_core.egg-info/requires.txt --- old/azure_cli_core-2.85.0/azure_cli_core.egg-info/requires.txt 2026-03-31 09:19:44.000000000 +0200 +++ new/azure_cli_core-2.86.0/azure_cli_core.egg-info/requires.txt 2026-05-01 02:41:23.000000000 +0200 @@ -7,7 +7,7 @@ jmespath knack~=0.11.0 microsoft-security-utilities-secret-masker~=1.0.0b4 -msal-extensions==1.2.0 +msal-extensions==1.3.1 packaging>=20.9 pkginfo>=1.5.0.1 PyJWT>=2.1.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.85.0/setup.py new/azure_cli_core-2.86.0/setup.py --- old/azure_cli_core-2.85.0/setup.py 2026-03-31 09:18:57.000000000 +0200 +++ new/azure_cli_core-2.86.0/setup.py 2026-05-01 02:40:32.000000000 +0200 @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = "2.85.0" +VERSION = "2.86.0" # If we have source, validate that our version numbers match # This should prevent uploading releases with mismatched versions. @@ -54,7 +54,7 @@ 'jmespath', 'knack~=0.11.0', 'microsoft-security-utilities-secret-masker~=1.0.0b4', - 'msal-extensions==1.2.0', + 'msal-extensions==1.3.1', 'msal[broker]==1.35.1; sys_platform == "win32"', 'msal==1.35.1; sys_platform != "win32"', 'packaging>=20.9',
