def test_add_extension_with_name_invalid_checksum(self): extension_name = MY_EXT_NAME bad_sha256 = 'thishashisclearlywrong' with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, bad_sha256)): with self.assertRaises(CLIError) as err: add_extension(cmd=self.cmd, extension_name=extension_name) self.assertTrue('The checksum of the extension does not match the expected value.' in str(err.exception))
def test_add_extension_with_name_valid_checksum(self): extension_name = MY_EXT_NAME computed_extension_sha256 = _compute_file_hash(MY_EXT_SOURCE) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, computed_extension_sha256)): add_extension(cmd=self.cmd, extension_name=extension_name) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_NAME], MY_EXT_NAME)
def test_update_extension_no_updates(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev') with mock.patch('azure.cli.core.extension.operations.resolve_from_index', side_effect=NoExtensionCandidatesError()): with self.assertRaises(CLIError) as err: update_extension(self.cmd, MY_EXT_NAME) self.assertTrue("No updates available for '{}'.".format(MY_EXT_NAME) in str(err.exception))
def test_add_list_show_remove_extension(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) actual = list_extensions() self.assertEqual(len(actual), 1) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_NAME], MY_EXT_NAME) remove_extension(MY_EXT_NAME) num_exts = len(list_extensions()) self.assertEqual(num_exts, 0)
def test_update_extension(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev') newer_extension = _get_test_data_file('myfirstcliextension-0.0.4+dev-py2.py3-none-any.whl') computed_extension_sha256 = _compute_file_hash(newer_extension) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(newer_extension, computed_extension_sha256)): update_extension(self.cmd, MY_EXT_NAME) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.4+dev')
def test_add_list_show_remove_extension_with_dashes(self): add_extension(cmd=self.cmd, source=MY_SECOND_EXT_SOURCE_DASHES) actual = list_extensions() self.assertEqual(len(actual), 1) ext = show_extension(MY_SECOND_EXT_NAME_DASHES) self.assertEqual(ext[OUT_KEY_NAME], MY_SECOND_EXT_NAME_DASHES) self.assertIn(OUT_KEY_NAME, ext[OUT_KEY_METADATA], "Unable to get full metadata") self.assertEqual(ext[OUT_KEY_METADATA][OUT_KEY_NAME], MY_SECOND_EXT_NAME_DASHES) remove_extension(MY_SECOND_EXT_NAME_DASHES) num_exts = len(list_extensions()) self.assertEqual(num_exts, 0)
def test_add_extension_verify_no_pip_proxy(self): extension_name = MY_EXT_NAME computed_extension_sha256 = _compute_file_hash(MY_EXT_SOURCE) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, computed_extension_sha256)), \ mock.patch('azure.cli.core.extension.operations.shutil'), \ mock.patch('azure.cli.core.extension.operations.check_output') as check_output: add_extension(cmd=self.cmd, extension_name=extension_name) args = check_output.call_args pip_cmd = args[0][0] if '--proxy' in pip_cmd: raise AssertionError("proxy parameter in check_output args although no proxy specified")
def test_update_extension_exception_in_update_and_rolled_back(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev') newer_extension = _get_test_data_file('myfirstcliextension-0.0.4+dev-py2.py3-none-any.whl') bad_sha256 = 'thishashisclearlywrong' with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(newer_extension, bad_sha256)): with self.assertRaises(CLIError) as err: update_extension(self.cmd, MY_EXT_NAME) self.assertTrue('Failed to update. Rolled {} back to {}.'.format(ext['name'], ext[OUT_KEY_VERSION]) in str(err.exception)) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev')
def test_add_extension_with_pip_proxy(self): extension_name = MY_EXT_NAME proxy_param = '--proxy' proxy_endpoint = "https://*****:*****@proxy.microsoft.com" computed_extension_sha256 = _compute_file_hash(MY_EXT_SOURCE) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, computed_extension_sha256)), \ mock.patch('azure.cli.core.extension.operations.shutil'), \ mock.patch('azure.cli.core.extension.operations.check_output') as check_output: add_extension(cmd=self.cmd, extension_name=extension_name, pip_proxy=proxy_endpoint) args = check_output.call_args pip_cmd = args[0][0] proxy_index = pip_cmd.index(proxy_param) assert pip_cmd[proxy_index + 1] == proxy_endpoint
def test_add_extension_with_name_but_it_already_exists(self): # Add extension without name first add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_NAME], MY_EXT_NAME) # Now add using name computed_extension_sha256 = _compute_file_hash(MY_EXT_SOURCE) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, computed_extension_sha256)): with mock.patch('azure.cli.core.extension.operations.logger') as mock_logger: add_extension(cmd=self.cmd, extension_name=MY_EXT_NAME) call_args = mock_logger.warning.call_args self.assertEqual("Extension '%s' is already installed.", call_args[0][0]) self.assertEqual(MY_EXT_NAME, call_args[0][1]) self.assertEqual(mock_logger.warning.call_count, 1)
def test_add_list_show_remove_extension_extra_index_url(self): """ Tests extension addition while specifying --extra-index-url parameter. :return: """ extra_index_urls = ['https://testpypi.python.org/simple', 'https://pypi.python.org/simple'] add_extension(cmd=self.cmd, source=MY_EXT_SOURCE, pip_extra_index_urls=extra_index_urls) actual = list_extensions() self.assertEqual(len(actual), 1) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_NAME], MY_EXT_NAME) remove_extension(MY_EXT_NAME) num_exts = len(list_extensions()) self.assertEqual(num_exts, 0)
def start_shell(cmd, update=None, style=None): from importlib import import_module try: get_extension(INTERACTIVE_EXTENSION_NAME) if update: logger.warning("Updating the Interactive extension to the latest available..") update_extension(INTERACTIVE_EXTENSION_NAME) reload_extension(INTERACTIVE_EXTENSION_NAME) except ExtensionNotInstalledException: logger.warning("Installing the Interactive extension..") add_extension(extension_name=INTERACTIVE_EXTENSION_NAME) add_extension_to_path(INTERACTIVE_EXTENSION_NAME) interactive_module = get_extension_modname(ext_name=INTERACTIVE_EXTENSION_NAME) azext_interactive = import_module(interactive_module) azext_interactive.start_shell(cmd, style=style)
def test_update_extension_extra_index_url(self): """ Tests extension update while specifying --extra-index-url parameter. :return: """ extra_index_urls = ['https://testpypi.python.org/simple', 'https://pypi.python.org/simple'] add_extension(cmd=self.cmd, source=MY_EXT_SOURCE, pip_extra_index_urls=extra_index_urls) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev') newer_extension = _get_test_data_file('myfirstcliextension-0.0.4+dev-py2.py3-none-any.whl') computed_extension_sha256 = _compute_file_hash(newer_extension) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(newer_extension, computed_extension_sha256)): update_extension(self.cmd, MY_EXT_NAME, pip_extra_index_urls=extra_index_urls) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.4+dev')
def test_update_extension_verify_no_pip_proxy(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev') newer_extension = _get_test_data_file('myfirstcliextension-0.0.4+dev-py2.py3-none-any.whl') computed_extension_sha256 = _compute_file_hash(newer_extension) with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, computed_extension_sha256)), \ mock.patch('azure.cli.core.extension.operations.shutil'), \ mock.patch('azure.cli.core.extension.operations.is_valid_sha256sum', return_value=(True, computed_extension_sha256)), \ mock.patch('azure.cli.core.extension.operations.extension_exists', return_value=None), \ mock.patch('azure.cli.core.extension.operations.check_output') as check_output: update_extension(self.cmd, MY_EXT_NAME) args = check_output.call_args pip_cmd = args[0][0] if '--proxy' in pip_cmd: raise AssertionError("proxy parameter in check_output args although no proxy specified")
def test_update_extension_with_pip_proxy(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) ext = show_extension(MY_EXT_NAME) self.assertEqual(ext[OUT_KEY_VERSION], '0.0.3+dev') newer_extension = _get_test_data_file('myfirstcliextension-0.0.4+dev-py2.py3-none-any.whl') computed_extension_sha256 = _compute_file_hash(newer_extension) proxy_param = '--proxy' proxy_endpoint = "https://*****:*****@proxy.microsoft.com" with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=(MY_EXT_SOURCE, computed_extension_sha256)), \ mock.patch('azure.cli.core.extension.operations.shutil'), \ mock.patch('azure.cli.core.extension.operations.is_valid_sha256sum', return_value=(True, computed_extension_sha256)), \ mock.patch('azure.cli.core.extension.operations.extension_exists', return_value=None), \ mock.patch('azure.cli.core.extension.operations.check_output') as check_output: update_extension(self.cmd, MY_EXT_NAME, pip_proxy=proxy_endpoint) args = check_output.call_args pip_cmd = args[0][0] proxy_index = pip_cmd.index(proxy_param) assert pip_cmd[proxy_index + 1] == proxy_endpoint
def _check_value(self, action, value): # pylint: disable=too-many-statements, too-many-locals, too-many-branches # 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 use_dynamic_install = 'no' if not self.command_source: candidates = [] args = self.prog.split() + self._raw_arguments use_dynamic_install = self._get_extension_use_dynamic_install_config( ) if use_dynamic_install != 'no': # Check if the command is from an extension from azure.cli.core.util import roughly_parse_command command_str = roughly_parse_command(args[1:]) ext_name = self._search_in_extension_commands(command_str) # The input command matches the prefix of one or more extension commands 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] use_dynamic_install = 'yes_without_prompt' except NoTTYException: error_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) logger.error(error_msg) telemetry.set_user_fault(error_msg) self.exit(2) else: ext_name = ext_name[0] if ext_name: caused_by_extension_not_installed = True telemetry.set_command_details( command_str, parameters=AzCliCommandInvoker. _extract_parameter_names(args), # pylint: disable=protected-access extension_name=ext_name) run_after_extension_installed = self._get_extension_run_after_dynamic_install_config( ) prompt_info = "" 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: prompt_info = " with prompt" logger.warning(NO_PROMPT_CONFIG_MSG) except NoTTYException: error_msg = "The command requires the extension {}. " \ "Unable to prompt for extension install confirmation as no tty " \ "available. {}".format(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( args, shell=platform.system() == 'Windows') error_msg = ( "Extension {} dynamically installed{} and commands will be " "rerun automatically.").format( ext_name, prompt_info) 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, prompt_info) 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 else error_msg 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) if not caused_by_extension_not_installed: candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) if candidates: # use the most likely candidate to replace the misspelled command args_inferred = [ item if item != value else candidates[0] for item in args ] command_name_inferred = ' '.join(args_inferred).split( '-')[0] 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 if not caused_by_extension_not_installed: recommender = CommandRecommender(*command_arguments, error_msg, cli_ctx) recommender.set_help_examples( self.get_examples(command_name_inferred)) recommendations = recommender.provide_recommendations() if recommendations: az_error.set_aladdin_recommendation(recommendations) # 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.print_error() az_error.send_telemetry() self.exit(2)
def test_add_extension_invalid(self): with self.assertRaises(ValueError): add_extension(cmd=self.cmd, source=MY_BAD_EXT_SOURCE) actual = list_extensions() self.assertEqual(len(actual), 0)
def test_add_extension_twice(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) num_exts = len(list_extensions()) self.assertEqual(num_exts, 1) with self.assertRaises(CLIError): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE)
def test_add_same_extension_user_system(self): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) num_exts = len(list_extensions()) self.assertEqual(num_exts, 1) with self.assertRaises(CLIError): add_extension(cmd=self.cmd, source=MY_EXT_SOURCE, system=True)
def test_add_extension_valid_whl_name_filenotfound(self): with self.assertRaises(CLIError): add_extension(cmd=self.cmd, source=_get_test_data_file('mywheel-0.0.3+dev-py2.py3-none-any.whl')) actual = list_extensions() self.assertEqual(len(actual), 0)
def test_add_extension_invalid_whl_name(self): with self.assertRaises(CLIError): add_extension(cmd=self.cmd, source=os.path.join('invalid', 'ext', 'path', 'file.whl')) actual = list_extensions() self.assertEqual(len(actual), 0)
def test_add_extension_with_name_source_not_whl(self): extension_name = 'myextension' with mock.patch('azure.cli.core.extension.operations.resolve_from_index', return_value=('{}.notwhl'.format(extension_name), None)): with self.assertRaises(ValueError) as err: add_extension(cmd=self.cmd, extension_name=extension_name) self.assertTrue('Unknown extension type. Only Python wheels are supported.' in str(err.exception))
def add_extension_cmd(source=None, extension_name=None, index_url=None, yes=None, pip_extra_index_urls=None, pip_proxy=None): return add_extension(source=source, extension_name=extension_name, index_url=index_url, yes=yes, pip_extra_index_urls=pip_extra_index_urls, pip_proxy=pip_proxy)