33# Licensed under the MIT License. See License.txt in the project root for license information.
44# --------------------------------------------------------------------------------------------
55
6- from enum import Enum
7-
86from azure .cli .core import telemetry
97from knack .log import get_logger
108
119
1210logger = get_logger (__name__ )
1311
1412
15- class AladdinUserFaultType (Enum ):
16- """Define the userfault types required by aladdin service
17- to get the command recommendations"""
18-
19- ExpectedArgument = 'ExpectedArgument'
20- UnrecognizedArguments = 'UnrecognizedArguments'
21- ValidationError = 'ValidationError'
22- UnknownSubcommand = 'UnknownSubcommand'
23- MissingRequiredParameters = 'MissingRequiredParameters'
24- MissingRequiredSubcommand = 'MissingRequiredSubcommand'
25- StorageAccountNotFound = 'StorageAccountNotFound'
26- Unknown = 'Unknown'
27- InvalidJMESPathQuery = 'InvalidJMESPathQuery'
28- InvalidOutputType = 'InvalidOutputType'
29- InvalidParameterValue = 'InvalidParameterValue'
30- UnableToParseCommandInput = 'UnableToParseCommandInput'
31- ResourceGroupNotFound = 'ResourceGroupNotFound'
32- InvalidDateTimeArgumentValue = 'InvalidDateTimeArgumentValue'
33- InvalidResourceGroupName = 'InvalidResourceGroupName'
34- AzureResourceNotFound = 'AzureResourceNotFound'
35- InvalidAccountName = 'InvalidAccountName'
36-
37-
38- def get_error_type (error_msg ):
39- """The the error type of the failed command from the error message.
40- The error types are only consumed by aladdin service for better recommendations.
41- """
42-
43- error_type = AladdinUserFaultType .Unknown
44- if not error_msg :
45- return error_type .value
46-
47- error_msg = error_msg .lower ()
48- if 'unrecognized' in error_msg :
49- error_type = AladdinUserFaultType .UnrecognizedArguments
50- elif 'expected one argument' in error_msg or 'expected at least one argument' in error_msg \
51- or 'value required' in error_msg :
52- error_type = AladdinUserFaultType .ExpectedArgument
53- elif 'misspelled' in error_msg :
54- error_type = AladdinUserFaultType .UnknownSubcommand
55- elif 'arguments are required' in error_msg or 'argument required' in error_msg :
56- error_type = AladdinUserFaultType .MissingRequiredParameters
57- if '_subcommand' in error_msg :
58- error_type = AladdinUserFaultType .MissingRequiredSubcommand
59- elif '_command_package' in error_msg :
60- error_type = AladdinUserFaultType .UnableToParseCommandInput
61- elif 'not found' in error_msg or 'could not be found' in error_msg \
62- or 'resource not found' in error_msg :
63- error_type = AladdinUserFaultType .AzureResourceNotFound
64- if 'storage_account' in error_msg or 'storage account' in error_msg :
65- error_type = AladdinUserFaultType .StorageAccountNotFound
66- elif 'resource_group' in error_msg or 'resource group' in error_msg :
67- error_type = AladdinUserFaultType .ResourceGroupNotFound
68- elif 'pattern' in error_msg or 'is not a valid value' in error_msg or 'invalid' in error_msg :
69- error_type = AladdinUserFaultType .InvalidParameterValue
70- if 'jmespath_type' in error_msg :
71- error_type = AladdinUserFaultType .InvalidJMESPathQuery
72- elif 'datetime_type' in error_msg :
73- error_type = AladdinUserFaultType .InvalidDateTimeArgumentValue
74- elif '--output' in error_msg :
75- error_type = AladdinUserFaultType .InvalidOutputType
76- elif 'resource_group' in error_msg :
77- error_type = AladdinUserFaultType .InvalidResourceGroupName
78- elif 'storage_account' in error_msg :
79- error_type = AladdinUserFaultType .InvalidAccountName
80- elif "validation error" in error_msg :
81- error_type = AladdinUserFaultType .ValidationError
82-
83- return error_type .value
84-
85-
8613class CommandRecommender : # pylint: disable=too-few-public-methods
8714 """Recommend a command for user when user's command fails.
88- It combines Aladdin recommendations and examples in help files."""
15+ It uses examples from help files to provide recommendations ."""
8916
9017 def __init__ (self , command , parameters , extension , error_msg , cli_ctx ):
9118 """
@@ -107,8 +34,6 @@ def __init__(self, command, parameters, extension, error_msg, cli_ctx):
10734 self .cli_ctx = cli_ctx
10835 # the item is a dict with the form {'command': #, 'description': #}
10936 self .help_examples = []
110- # the item is a dict with the form {'command': #, 'description': #, 'link': #}
111- self .aladdin_recommendations = []
11237
11338 def set_help_examples (self , examples ):
11439 """Set help examples.
@@ -119,89 +44,10 @@ def set_help_examples(self, examples):
11944
12045 self .help_examples .extend (examples )
12146
122- def _set_aladdin_recommendations (self ): # pylint: disable=too-many-locals
123- """Set Aladdin recommendations.
124- Call the API, parse the response and set aladdin_recommendations.
125- """
126-
127- import hashlib
128- import json
129- import requests
130- from requests import RequestException
131- from http import HTTPStatus
132- from azure .cli .core import __version__ as version
133-
134- api_url = 'https://app.aladdin.microsoft.com/api/v1.0/suggestions'
135- correlation_id = telemetry ._session .correlation_id # pylint: disable=protected-access
136- subscription_id = telemetry ._get_azure_subscription_id () # pylint: disable=protected-access
137- event_id = telemetry ._session .event_id # pylint: disable=protected-access
138- # Used for DDOS protection and rate limiting
139- user_id = telemetry ._get_user_azure_id () # pylint: disable=protected-access
140- hashed_user_id = hashlib .sha256 (user_id .encode ('utf-8' )).hexdigest ()
141-
142- headers = {
143- 'Content-Type' : 'application/json' ,
144- 'X-UserId' : hashed_user_id
145- }
146- context = {
147- 'versionNumber' : version ,
148- 'errorType' : get_error_type (self .error_msg )
149- }
150-
151- if telemetry .is_telemetry_enabled ():
152- if correlation_id :
153- context ['correlationId' ] = correlation_id
154- if subscription_id :
155- context ['subscriptionId' ] = subscription_id
156- if event_id :
157- context ['eventId' ] = event_id
158-
159- parameters = self ._normalize_parameters (self .parameters )
160- parameters = [item for item in set (parameters ) if item not in ['--debug' , '--verbose' , '--only-show-errors' ]]
161- query = {
162- "command" : self .command ,
163- "parameters" : ',' .join (parameters )
164- }
165-
166- response = None
167- try :
168- response = requests .get (
169- api_url ,
170- params = {
171- 'query' : json .dumps (query ),
172- 'clientType' : 'AzureCli' ,
173- 'context' : json .dumps (context )
174- },
175- headers = headers ,
176- timeout = 1 )
177- telemetry .set_debug_info ('AladdinResponseTime' , response .elapsed .total_seconds ())
178-
179- except RequestException as ex :
180- logger .debug ('Recommendation requests.get() exception: %s' , ex )
181- telemetry .set_debug_info ('AladdinException' , ex .__class__ .__name__ )
182-
183- recommendations = []
184- if response and response .status_code == HTTPStatus .OK :
185- for result in response .json ():
186- # parse the response to get the raw command
187- raw_command = 'az {} ' .format (result ['command' ])
188- for parameter , placeholder in zip (result ['parameters' ].split (',' ), result ['placeholders' ].split ('♠' )):
189- raw_command += '{} {}{}' .format (parameter , placeholder , ' ' if placeholder else '' )
190-
191- # format the recommendation
192- recommendation = {
193- 'command' : raw_command .strip (),
194- 'description' : result ['description' ],
195- 'link' : result ['link' ]
196- }
197- recommendations .append (recommendation )
198-
199- self .aladdin_recommendations .extend (recommendations )
200-
20147 def provide_recommendations (self ):
20248 """Provide recommendations when a command fails.
20349
204- The recommendations are either from Aladdin service or CLI help examples,
50+ The recommendations are from CLI help examples,
20551 which include both commands and reference links along with their descriptions.
20652
20753 :return: The decorated recommendations
@@ -273,14 +119,7 @@ def replace_param_values(command): # pylint: disable=unused-variable
273119 if self .cli_ctx and self .cli_ctx .config .get ('core' , 'error_recommendation' , 'on' ).upper () == 'OFF' :
274120 return []
275121
276- # get recommendations from Aladdin service
277- if not self ._disable_aladdin_service ():
278- self ._set_aladdin_recommendations ()
279-
280- # recommendations are either all from Aladdin or all from help examples
281- recommendations = self .aladdin_recommendations
282- if not recommendations :
283- recommendations = self .help_examples
122+ recommendations = self .help_examples
284123
285124 # sort the recommendations by parameter matching, get the top 3 recommended commands
286125 recommendations = sort_recommendations (recommendations )[:3 ]
@@ -305,8 +144,6 @@ def replace_param_values(command): # pylint: disable=unused-variable
305144
306145 # add reference link as a recommendation
307146 decorated_link = [(Style .HYPERLINK , OVERVIEW_REFERENCE )]
308- if self .aladdin_recommendations :
309- decorated_link = [(Style .HYPERLINK , self .aladdin_recommendations [0 ]['link' ])]
310147
311148 decorated_description = [(Style .SECONDARY , 'Read more about the command in reference docs' )]
312149 decorated_recommendations .append ((decorated_link , decorated_description ))
@@ -319,49 +156,11 @@ def replace_param_values(command): # pylint: disable=unused-variable
319156 def _set_recommended_command_to_telemetry (self , raw_commands ):
320157 """Set the recommended commands to Telemetry
321158
322- Aladdin recommended commands and commands from CLI help examples are
323- set to different properties in Telemetry.
324-
325159 :param raw_commands: The recommended raw commands
326160 :type raw_commands: list
327161 """
328162
329- if self .aladdin_recommendations :
330- telemetry .set_debug_info ('AladdinRecommendCommand' , ';' .join (raw_commands ))
331- else :
332- telemetry .set_debug_info ('ExampleRecommendCommand' , ';' .join (raw_commands ))
333-
334- def _disable_aladdin_service (self ):
335- """Decide whether to disable aladdin request when a command fails.
336-
337- The possible cases to disable it are:
338- 1. CLI context is missing
339- 2. In air-gapped clouds
340- 3. In testing environments
341- 4. In autocomplete mode
342-
343- :return: whether Aladdin service need to be disabled or not
344- :type: bool
345- """
346-
347- from azure .cli .core .cloud import CLOUDS_FORBIDDING_ALADDIN_REQUEST
348-
349- # CLI is not started well
350- if not self .cli_ctx or not self .cli_ctx .cloud :
351- return True
352-
353- # for air-gapped clouds
354- if self .cli_ctx .cloud .name in CLOUDS_FORBIDDING_ALADDIN_REQUEST :
355- return True
356-
357- # for testing environments
358- if self .cli_ctx .__class__ .__name__ == 'DummyCli' :
359- return True
360-
361- if self .cli_ctx .data ['completer_active' ]:
362- return True
363-
364- return False
163+ telemetry .set_debug_info ('ExampleRecommendCommand' , ';' .join (raw_commands ))
365164
366165 def _normalize_parameters (self , args ):
367166 """Normalize a parameter list.
0 commit comments