def test_extract_parameters_correctly(self): from azure.cli.core.commands import AzCliCommandInvoker args = ['vm', 'user', 'update', '-g', 'rg', '-n', 'vm1', '-u', 'user', '--ssh-key-value', '-----BEGIN PRIVATE KEY-----'] self.assertEqual(['-g', '-n', '-u', '--ssh-key-value'], AzCliCommandInvoker._extract_parameter_names(args)) args = ['vm', 'create', '--resource-group-name', 'rg', '--name', 'vm1', '--image', 'centos'] self.assertEqual(['--resource-group-name', '--name', '--image'], AzCliCommandInvoker._extract_parameter_names(args)) args = ['vm', 'show', '-g', 'rg', '--name', 'vm1', '-d', '--debug'] self.assertEqual(['-g', '--name', '-d', '--debug'], AzCliCommandInvoker._extract_parameter_names(args))
def _normalize_parameters(self, args): """Normalize a parameter list. Get the standard parameter name list of the raw parameters, which includes: 1. Use long options to replace short options 2. Remove the unrecognized parameter names 3. Sort the parameter names by their lengths An example: ['-g', 'RG', '-n', 'NAME'] ==> ['--resource-group', '--name'] :param args: The raw arg list of a command :type args: list :return: A standard, valid and sorted parameter name list :type: list """ from azure.cli.core.commands import AzCliCommandInvoker parameters = AzCliCommandInvoker._extract_parameter_names(args) # pylint: disable=protected-access normalized_parameters = [] param_mappings = self._get_param_mappings() for parameter in parameters: if parameter in param_mappings: normalized_form = param_mappings.get(parameter, None) or parameter normalized_parameters.append(normalized_form) else: logger.debug('"%s" is an invalid parameter for command "%s".', parameter, self.command) return sorted(normalized_parameters)
def _extract_parameter_names(self, parameters): # pylint: disable=no-self-use """Extract parameter names from the raw parameters. An example: ['-g', 'RG', '-n', 'NAME'] ==> ['-g', '-n'] """ from azure.cli.core.commands import AzCliCommandInvoker return AzCliCommandInvoker._extract_parameter_names(parameters) # pylint: disable=protected-access
def extract_safe_params(parameters): return AzCliCommandInvoker._extract_parameter_names(parameters) # pylint: disable=protected-access
def _check_value(self, action, value): # pylint: disable=too-many-statements, too-many-locals # Override to customize the error message when a argument is not among the available choices # converted value must be one of the choices (if specified) if action.choices is not None and value not in action.choices: # pylint: disable=too-many-nested-blocks # self.cli_ctx is None when self.prog is beyond 'az', such as 'az iot'. # use cli_ctx from cli_help which is not lost. cli_ctx = self.cli_ctx or (self.cli_help.cli_ctx if self.cli_help else None) caused_by_extension_not_installed = False command_name_inferred = self.prog error_msg = None if not self.command_source: candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) if candidates: # use the most likely candidate to replace the misspelled command args = self.prog.split() + self._raw_arguments args_inferred = [item if item != value else candidates[0] for item in args] command_name_inferred = ' '.join(args_inferred).split('-')[0] use_dynamic_install = self._get_extension_use_dynamic_install_config() if use_dynamic_install != 'no' and not candidates: # Check if the command is from an extension from azure.cli.core.util import roughly_parse_command cmd_list = self.prog.split() + self._raw_arguments command_str = roughly_parse_command(cmd_list[1:]) ext_name = self._search_in_extension_commands(command_str) if ext_name: caused_by_extension_not_installed = True telemetry.set_command_details(command_str, parameters=AzCliCommandInvoker._extract_parameter_names(cmd_list), # pylint: disable=protected-access extension_name=ext_name) run_after_extension_installed = self._get_extension_run_after_dynamic_install_config() if use_dynamic_install == 'yes_without_prompt': logger.warning('The command requires the extension %s. ' 'It will be installed first.', ext_name) go_on = True else: from knack.prompting import prompt_y_n, NoTTYException prompt_msg = 'The command requires the extension {}. ' \ 'Do you want to install it now?'.format(ext_name) if run_after_extension_installed: prompt_msg = '{} The command will continue to run after the extension is installed.' \ .format(prompt_msg) NO_PROMPT_CONFIG_MSG = "Run 'az config set extension.use_dynamic_install=" \ "yes_without_prompt' to allow installing extensions without prompt." try: go_on = prompt_y_n(prompt_msg, default='y') if go_on: logger.warning(NO_PROMPT_CONFIG_MSG) except NoTTYException: logger.warning("The command requires the extension %s.\n " "Unable to prompt for extension install confirmation as no tty " "available. %s", ext_name, NO_PROMPT_CONFIG_MSG) go_on = False if go_on: from azure.cli.core.extension.operations import add_extension add_extension(cli_ctx=cli_ctx, extension_name=ext_name, upgrade=True) if run_after_extension_installed: import subprocess import platform exit_code = subprocess.call(cmd_list, shell=platform.system() == 'Windows') error_msg = ("Extension {} dynamically installed and commands will be " "rerun automatically.").format(ext_name) telemetry.set_user_fault(error_msg) self.exit(exit_code) else: with CommandLoggerContext(logger): error_msg = 'Extension {} installed. Please rerun your command.'.format(ext_name) logger.error(error_msg) telemetry.set_user_fault(error_msg) self.exit(2) else: error_msg = "The command requires the latest version of extension {ext_name}. " \ "To install, run 'az extension add --upgrade -n {ext_name}'.".format(ext_name=ext_name) if not error_msg: # parser has no `command_source`, value is part of command itself error_msg = "'{value}' is misspelled or not recognized by the system.".format(value=value) az_error = CommandNotFoundError(error_msg) else: # `command_source` indicates command values have been parsed, value is an argument parameter = action.option_strings[0] if action.option_strings else action.dest error_msg = "{prog}: '{value}' is not a valid value for '{param}'.".format( prog=self.prog, value=value, param=parameter) candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) az_error = InvalidArgumentValueError(error_msg) command_arguments = self._get_failure_recovery_arguments(action) if candidates: az_error.set_recommendation("Did you mean '{}' ?".format(candidates[0])) # recommend a command for user recommender = CommandRecommender(*command_arguments, error_msg, cli_ctx) recommender.set_help_examples(self.get_examples(command_name_inferred)) recommended_command = recommender.recommend_a_command() if recommended_command: az_error.set_recommendation("Try this: '{}'".format(recommended_command)) # remind user to check extensions if we can not find a command to recommend if isinstance(az_error, CommandNotFoundError) \ and not az_error.recommendations and self.prog == 'az' \ and use_dynamic_install == 'no': az_error.set_recommendation(EXTENSION_REFERENCE) az_error.set_recommendation(OVERVIEW_REFERENCE.format(command=self.prog)) if not caused_by_extension_not_installed: az_error.print_error() az_error.send_telemetry() self.exit(2)
def _check_value_in_extensions(cli_ctx, parser, args, no_prompt): # pylint: disable=too-many-statements, too-many-locals """Check if the command args can be found in extension commands. Exit command if the error is caused by an extension not installed. Otherwise return. """ # Check if the command is from an extension from azure.cli.core.util import roughly_parse_command from azure.cli.core.azclierror import NoTTYError exit_code = 2 command_str = roughly_parse_command(args[1:]) allow_prefix_match = args[-1] == '-h' or args[-1] == '--help' ext_name = _search_in_extension_commands(cli_ctx, command_str, allow_prefix_match=allow_prefix_match) # ext_name is a list if the input command matches the prefix of one or more extension commands, # for instance: `az blueprint` when running `az blueprint -h` # ext_name is a str if the input command matches a complete command of an extension, # for instance: `az blueprint create` if isinstance(ext_name, list): if len(ext_name) > 1: from knack.prompting import prompt_choice_list, NoTTYException prompt_msg = "The command requires the latest version of one of the following " \ "extensions. You need to pick one to install:" try: choice_idx = prompt_choice_list(prompt_msg, ext_name) ext_name = ext_name[choice_idx] no_prompt = True except NoTTYException: tty_err_msg = "{}{}\nUnable to prompt for selection as no tty available. Please update or " \ "install the extension with 'az extension add --upgrade -n <extension-name>'." \ .format(prompt_msg, ext_name) az_error = NoTTYError(tty_err_msg) az_error.print_error() az_error.send_telemetry() parser.exit(exit_code) else: ext_name = ext_name[0] if not ext_name: return # If a valid command has parser error, it may be caused by CLI running on a profile that is # not 'latest' and the command is not supported in that profile. If this command exists in an extension, # CLI will try to download the extension and rerun the command. But the parser will fail again and try to # install the extension and rerun the command infinitely. So we need to check if the latest version of the # extension is already installed and return if yes as the error is not caused by extension not installed. from azure.cli.core.extension import get_extension, ExtensionNotInstalledException from azure.cli.core.extension._resolve import resolve_from_index, NoExtensionCandidatesError try: ext = get_extension(ext_name) except ExtensionNotInstalledException: pass else: try: resolve_from_index(ext_name, cur_version=ext.version, cli_ctx=cli_ctx) except NoExtensionCandidatesError: return telemetry.set_command_details(command_str, parameters=AzCliCommandInvoker._extract_parameter_names(args), # pylint: disable=protected-access extension_name=ext_name) run_after_extension_installed = _get_extension_run_after_dynamic_install_config(cli_ctx) prompt_info = "" if no_prompt: logger.warning('The command requires the extension %s. It will be installed first.', ext_name) install_ext = True else: # yes_prompt from knack.prompting import prompt_y_n, NoTTYException prompt_msg = 'The command requires the extension {}. Do you want to install it now?'.format(ext_name) if run_after_extension_installed: prompt_msg = '{} The command will continue to run after the extension is installed.' \ .format(prompt_msg) NO_PROMPT_CONFIG_MSG = "Run 'az config set extension.use_dynamic_install=" \ "yes_without_prompt' to allow installing extensions without prompt." try: install_ext = prompt_y_n(prompt_msg, default='y') if install_ext: prompt_info = " with prompt" logger.warning(NO_PROMPT_CONFIG_MSG) except NoTTYException: tty_err_msg = "The command requires the extension {}. " \ "Unable to prompt for extension install confirmation as no tty " \ "available. {}".format(ext_name, NO_PROMPT_CONFIG_MSG) az_error = NoTTYError(tty_err_msg) az_error.print_error() az_error.send_telemetry() parser.exit(exit_code) print_error = True if install_ext: from azure.cli.core.extension.operations import add_extension add_extension(cli_ctx=cli_ctx, extension_name=ext_name, upgrade=True) if run_after_extension_installed: import subprocess import platform exit_code = subprocess.call(args, shell=platform.system() == 'Windows') # In this case, error msg is for telemetry recording purpose only. # From UX perspective, the command will rerun in subprocess. Whether it succeeds or fails, # mesages will be shown from the subprocess and this process should not print more message to # interrupt that. print_error = False error_msg = ("Extension {} dynamically installed{} and commands will be " "rerun automatically.").format(ext_name, prompt_info) else: error_msg = 'Extension {} installed{}. Please rerun your command.' \ .format(ext_name, prompt_info) else: error_msg = "The command requires the latest version of extension {ext_name}. " \ "To install, run 'az extension add --upgrade -n {ext_name}'.".format( ext_name=ext_name) az_error = CommandNotFoundError(error_msg) if print_error: az_error.print_error() az_error.send_telemetry() parser.exit(exit_code)