diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b3bb0de1ade..7d1b55c0aa8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -21,7 +21,7 @@ /src/azure-cli/azure/cli/command_modules/container/ @samkreter /src/azure-cli/azure/cli/command_modules/consumption/ @sandeepnl /src/azure-cli/azure/cli/command_modules/dls/ @lewu-msft -/src/azure-cli/azure/cli/command_modules/extension/ @zikalino +/src/azure-cli/azure/cli/command_modules/extension/ @fengzhou-msft @haroldrandom /src/azure-cli/azure/cli/command_modules/keyvault/ @bim-msft @fengzhou-msft /src/azure-cli/azure/cli/command_modules/monitor/ @MyronFanQiu /src/azure-cli/azure/cli/command_modules/natgateway/ @khannarheams @MyronFanQiu @haroldrandom diff --git a/src/azure-cli-core/azure/cli/core/extension/__init__.py b/src/azure-cli-core/azure/cli/core/extension/__init__.py index 4104f77040e..83d478e858e 100644 --- a/src/azure-cli-core/azure/cli/core/extension/__init__.py +++ b/src/azure-cli-core/azure/cli/core/extension/__init__.py @@ -2,12 +2,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long import os import traceback import json import re -import sys import pkginfo from azure.cli.core._config import GLOBAL_CONFIG_DIR, ENV_VAR_PREFIX @@ -18,10 +18,11 @@ az_config = CLIConfig(config_dir=GLOBAL_CONFIG_DIR, config_env_var_prefix=ENV_VAR_PREFIX) _CUSTOM_EXT_DIR = az_config.get('extension', 'dir', None) _DEV_EXTENSION_SOURCES = az_config.get('extension', 'dev_sources', None) +_CUSTOM_EXT_SYS_DIR = az_config.get('extension', 'sys_dir', None) EXTENSIONS_DIR = os.path.expanduser(_CUSTOM_EXT_DIR) if _CUSTOM_EXT_DIR else os.path.join(GLOBAL_CONFIG_DIR, 'cliextensions') DEV_EXTENSION_SOURCES = _DEV_EXTENSION_SOURCES.split(',') if _DEV_EXTENSION_SOURCES else [] -EXTENSIONS_SYS_DIR = os.path.join(get_python_lib(), 'azure-cli-extensions') if sys.platform.startswith('linux') else "" +EXTENSIONS_SYS_DIR = os.path.expanduser(_CUSTOM_EXT_SYS_DIR) if _CUSTOM_EXT_SYS_DIR else os.path.join(get_python_lib(), 'azure-cli-extensions') EXTENSIONS_MOD_PREFIX = 'azext_' @@ -134,10 +135,11 @@ def get_version(self): def get_metadata(self): from glob import glob - if not extension_exists(self.name): - return None metadata = {} ext_dir = self.path or get_extension_path(self.name) + + if not ext_dir or not os.path.isdir(ext_dir): + return None info_dirs = glob(os.path.join(ext_dir, self.name.replace('-', '_') + '-' + '*.dist-info')) azext_metadata = WheelExtension.get_azext_metadata(ext_dir) @@ -199,11 +201,10 @@ def get_version(self): return self.metadata.get('version') def get_metadata(self): - - if not extension_exists(self.name): - return None metadata = {} ext_dir = self.path + if not ext_dir or not os.path.isdir(ext_dir): + return None egg_info_dirs = [f for f in os.listdir(ext_dir) if f.endswith('.egg-info')] azext_metadata = DevExtension.get_azext_metadata(ext_dir) if azext_metadata: @@ -284,8 +285,16 @@ def get_extension_modname(ext_name=None, ext_dir=None): def get_extension_path(ext_name): + # This will return the path for a WHEEL extension if exists. + ext_sys_path = os.path.join(EXTENSIONS_SYS_DIR, ext_name) + ext_path = os.path.join(EXTENSIONS_DIR, ext_name) + return ext_path if os.path.isdir(ext_path) else ( + ext_sys_path if os.path.isdir(ext_sys_path) else None) + + +def build_extension_path(ext_name, system=None): # This will simply form the path for a WHEEL extension. - return os.path.join(EXTENSIONS_DIR, ext_name) + return os.path.join(EXTENSIONS_SYS_DIR, ext_name) if system else os.path.join(EXTENSIONS_DIR, ext_name) def get_extensions(ext_type=None): diff --git a/src/azure-cli-core/azure/cli/core/extension/operations.py b/src/azure-cli-core/azure/cli/core/extension/operations.py index 97a5aea0e02..1268785cddd 100644 --- a/src/azure-cli-core/azure/cli/core/extension/operations.py +++ b/src/azure-cli-core/azure/cli/core/extension/operations.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long + from collections import OrderedDict import sys import os @@ -17,7 +19,7 @@ from pkg_resources import parse_version from azure.cli.core.util import CLIError, reload_module -from azure.cli.core.extension import (extension_exists, get_extension_path, get_extensions, get_extension_modname, +from azure.cli.core.extension import (extension_exists, build_extension_path, get_extensions, get_extension_modname, get_extension, ext_compat_with_cli, EXT_METADATA_ISPREVIEW, EXT_METADATA_ISEXPERIMENTAL, WheelExtension, DevExtension, ExtensionNotInstalledException, WHEEL_INFO_RE) @@ -37,13 +39,14 @@ OUT_KEY_METADATA = 'metadata' OUT_KEY_PREVIEW = 'preview' OUT_KEY_EXPERIMENTAL = 'experimental' +OUT_KEY_PATH = 'path' IS_WINDOWS = sys.platform.lower() in ['windows', 'win32'] LIST_FILE_PATH = os.path.join(os.sep, 'etc', 'apt', 'sources.list.d', 'azure-cli.list') LSB_RELEASE_FILE = os.path.join(os.sep, 'etc', 'lsb-release') -def _run_pip(pip_exec_args): +def _run_pip(pip_exec_args, extension_path): cmd = [sys.executable, '-m', 'pip'] + pip_exec_args + ['-vv', '--disable-pip-version-check', '--no-cache-dir'] logger.debug('Running: %s', cmd) try: @@ -53,6 +56,8 @@ def _run_pip(pip_exec_args): except CalledProcessError as e: logger.debug(e.output) logger.debug(e) + if "PermissionError: [WinError 5]" in e.output: + logger.warning("You do not have the permission to add extensions in the target directory: %s. You may need to rerun on a shell as administrator.", os.path.split(extension_path)[0]) returncode = e.returncode return returncode @@ -79,7 +84,7 @@ def _validate_whl_extension(ext_file): check_version_compatibility(azext_metadata) -def _add_whl_ext(cmd, source, ext_sha256=None, pip_extra_index_urls=None, pip_proxy=None): # pylint: disable=too-many-statements +def _add_whl_ext(cmd, source, ext_sha256=None, pip_extra_index_urls=None, pip_proxy=None, system=None): # pylint: disable=too-many-statements cmd.cli_ctx.get_progress_controller().add(message='Analyzing') if not source.endswith('.whl'): raise ValueError('Unknown extension type. Only Python wheels are supported.') @@ -135,7 +140,7 @@ def _add_whl_ext(cmd, source, ext_sha256=None, pip_extra_index_urls=None, pip_pr check_distro_consistency() cmd.cli_ctx.get_progress_controller().add(message='Installing') # Install with pip - extension_path = get_extension_path(extension_name) + extension_path = build_extension_path(extension_name, system) pip_args = ['install', '--target', extension_path, ext_file] if pip_proxy: @@ -146,7 +151,7 @@ def _add_whl_ext(cmd, source, ext_sha256=None, pip_extra_index_urls=None, pip_pr logger.debug('Executing pip with args: %s', pip_args) with HomebrewPipPatch(): - pip_status_code = _run_pip(pip_args) + pip_status_code = _run_pip(pip_args, extension_path) if pip_status_code > 0: logger.debug('Pip failed so deleting anything we might have installed at %s', extension_path) shutil.rmtree(extension_path, ignore_errors=True) @@ -168,12 +173,12 @@ def is_valid_sha256sum(a_file, expected_sum): return expected_sum == computed_hash, computed_hash -def _augment_telemetry_with_ext_info(extension_name): +def _augment_telemetry_with_ext_info(extension_name, ext=None): # The extension must be available before calling this otherwise we can't get the version from metadata if not extension_name: return try: - ext = get_extension(extension_name) + ext = ext or get_extension(extension_name) ext_version = ext.version set_extension_management_detail(extension_name, ext_version) except Exception: # nopa pylint: disable=broad-except @@ -200,7 +205,7 @@ def check_version_compatibility(azext_metadata): 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): + pip_extra_index_urls=None, pip_proxy=None, system=None): ext_sha256 = None if extension_name: cmd.cli_ctx.get_progress_controller().add(message='Searching') @@ -220,13 +225,14 @@ def add_extension(cmd, source=None, extension_name=None, index_url=None, yes=Non logger.debug(err) raise CLIError("No matching extensions for '{}'. Use --debug for more information.".format(extension_name)) 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) - _augment_telemetry_with_ext_info(extension_name) + pip_extra_index_urls=pip_extra_index_urls, pip_proxy=pip_proxy, system=system) try: - if extension_name and get_extension(extension_name).experimental: + ext = get_extension(extension_name) + _augment_telemetry_with_ext_info(extension_name, ext) + if extension_name and ext.experimental: logger.warning("The installed extension '%s' is experimental and not covered by customer support. " "Please use with discretion.", extension_name) - elif extension_name and get_extension(extension_name).preview: + elif extension_name and ext.preview: logger.warning("The installed extension '%s' is in preview.", extension_name) except ExtensionNotInstalledException: pass @@ -245,15 +251,15 @@ def log_err(func, path, exc_info): "Extension '{name}' was installed in development mode. Remove using " "`azdev extension remove {name}`".format(name=extension_name)) # 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) - shutil.rmtree(get_extension_path(extension_name), onerror=log_err) + _augment_telemetry_with_ext_info(extension_name, ext) + shutil.rmtree(ext.path, onerror=log_err) except ExtensionNotInstalledException as e: raise CLIError(e) def list_extensions(): return [{OUT_KEY_NAME: ext.name, OUT_KEY_VERSION: ext.version, OUT_KEY_TYPE: ext.ext_type, - OUT_KEY_PREVIEW: ext.preview, OUT_KEY_EXPERIMENTAL: ext.experimental} + OUT_KEY_PREVIEW: ext.preview, OUT_KEY_EXPERIMENTAL: ext.experimental, OUT_KEY_PATH: ext.path} for ext in get_extensions()] @@ -263,7 +269,8 @@ def show_extension(extension_name): return {OUT_KEY_NAME: extension.name, OUT_KEY_VERSION: extension.version, OUT_KEY_TYPE: extension.ext_type, - OUT_KEY_METADATA: extension.metadata} + OUT_KEY_METADATA: extension.metadata, + OUT_KEY_PATH: extension.path} except ExtensionNotInstalledException as e: raise CLIError(e) @@ -279,7 +286,7 @@ def update_extension(cmd, extension_name, index_url=None, pip_extra_index_urls=N raise CLIError("No updates available for '{}'. Use --debug for more information.".format(extension_name)) # Copy current version of extension to tmp directory in case we need to restore it after a failed install. backup_dir = os.path.join(tempfile.mkdtemp(), extension_name) - extension_path = get_extension_path(extension_name) + extension_path = ext.path logger.debug('Backing up the current extension: %s to %s', extension_path, backup_dir) shutil.copytree(extension_path, backup_dir) # Remove current version of the extension diff --git a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py index 2d617be2c7f..a7f9ef3bf29 100644 --- a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py +++ b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py @@ -10,9 +10,11 @@ import mock from azure.cli.core.util import CLIError +from azure.cli.core.extension import build_extension_path from azure.cli.core.extension.operations import (list_extensions, add_extension, show_extension, remove_extension, update_extension, - list_available_extensions, OUT_KEY_NAME, OUT_KEY_VERSION, OUT_KEY_METADATA) + list_available_extensions, OUT_KEY_NAME, OUT_KEY_VERSION, + OUT_KEY_METADATA, OUT_KEY_PATH) from azure.cli.core.extension._resolve import NoExtensionCandidatesError from azure.cli.core.mock import DummyCli @@ -39,13 +41,18 @@ class TestExtensionCommands(unittest.TestCase): def setUp(self): self.ext_dir = tempfile.mkdtemp() - self.patcher = mock.patch('azure.cli.core.extension.EXTENSIONS_DIR', self.ext_dir) - self.patcher.start() + self.ext_sys_dir = tempfile.mkdtemp() + self.patchers = [mock.patch('azure.cli.core.extension.EXTENSIONS_DIR', self.ext_dir), + mock.patch('azure.cli.core.extension.EXTENSIONS_SYS_DIR', self.ext_sys_dir)] + for patcher in self.patchers: + patcher.start() self.cmd = self._setup_cmd() def tearDown(self): - self.patcher.stop() + for patcher in self.patchers: + patcher.stop() shutil.rmtree(self.ext_dir, ignore_errors=True) + shutil.rmtree(self.ext_sys_dir, ignore_errors=True) def test_no_extensions_dir(self): shutil.rmtree(self.ext_dir) @@ -66,6 +73,34 @@ def test_add_list_show_remove_extension(self): num_exts = len(list_extensions()) self.assertEqual(num_exts, 0) + def test_add_list_show_remove_system_extension(self): + add_extension(cmd=self.cmd, source=MY_EXT_SOURCE, system=True) + 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_add_list_show_remove_user_system_extensions(self): + add_extension(cmd=self.cmd, source=MY_EXT_SOURCE) + add_extension(cmd=self.cmd, source=MY_SECOND_EXT_SOURCE_DASHES, system=True) + actual = list_extensions() + self.assertEqual(len(actual), 2) + ext = show_extension(MY_EXT_NAME) + self.assertEqual(ext[OUT_KEY_NAME], MY_EXT_NAME) + self.assertEqual(ext[OUT_KEY_PATH], build_extension_path(MY_EXT_NAME)) + second_ext = show_extension(MY_SECOND_EXT_NAME_DASHES) + self.assertEqual(second_ext[OUT_KEY_NAME], MY_SECOND_EXT_NAME_DASHES) + self.assertEqual(second_ext[OUT_KEY_PATH], build_extension_path(MY_SECOND_EXT_NAME_DASHES, system=True)) + remove_extension(MY_EXT_NAME) + num_exts = len(list_extensions()) + self.assertEqual(num_exts, 1) + remove_extension(MY_SECOND_EXT_NAME_DASHES) + num_exts = len(list_extensions()) + self.assertEqual(num_exts, 0) + def test_add_list_show_remove_extension_with_dashes(self): add_extension(cmd=self.cmd, source=MY_SECOND_EXT_SOURCE_DASHES) actual = list_extensions() @@ -85,6 +120,13 @@ def test_add_extension_twice(self): 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_invalid(self): with self.assertRaises(ValueError): add_extension(cmd=self.cmd, source=MY_BAD_EXT_SOURCE) diff --git a/src/azure-cli-core/azure/cli/core/tests/data/my_second_cli_extension.zip b/src/azure-cli-core/azure/cli/core/tests/data/my_second_cli_extension.zip new file mode 100644 index 00000000000..be255e57140 Binary files /dev/null and b/src/azure-cli-core/azure/cli/core/tests/data/my_second_cli_extension.zip differ diff --git a/src/azure-cli-core/azure/cli/core/tests/test_extension.py b/src/azure-cli-core/azure/cli/core/tests/test_extension.py index cc2695b798c..f7c9d4fa1b8 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_extension.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_extension.py @@ -10,7 +10,7 @@ import mock -from azure.cli.core.extension import (get_extensions, get_extension_path, extension_exists, +from azure.cli.core.extension import (get_extensions, build_extension_path, extension_exists, get_extension, get_extension_names, get_extension_modname, ext_compat_with_cli, ExtensionNotInstalledException, WheelExtension, EXTENSIONS_MOD_PREFIX, EXT_METADATA_MINCLICOREVERSION, EXT_METADATA_MAXCLICOREVERSION) @@ -19,25 +19,34 @@ # The test extension name EXT_NAME = 'myfirstcliextension' EXT_VERSION = '0.0.3+dev' +SECOND_EXT_NAME = 'my_second_cli_extension' def _get_test_data_file(filename): return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', filename) -def _install_test_extension1(): # pylint: disable=no-self-use +def _install_test_extension1(system=None): # pylint: disable=no-self-use # We extract the extension into place as we aren't testing install here zip_file = _get_test_data_file('{}.zip'.format(EXT_NAME)) zip_ref = zipfile.ZipFile(zip_file, 'r') - zip_ref.extractall(get_extension_path(EXT_NAME)) + zip_ref.extractall(build_extension_path(EXT_NAME, system=system)) zip_ref.close() -def _install_test_extension2(): # pylint: disable=no-self-use +def _install_test_extension2(system=None): # pylint: disable=no-self-use # We extract the extension into place as we aren't testing install here zip_file = _get_test_data_file('myfirstcliextension_az_extmetadata.zip') zip_ref = zipfile.ZipFile(zip_file, 'r') - zip_ref.extractall(get_extension_path(EXT_NAME)) + zip_ref.extractall(build_extension_path(EXT_NAME, system=system)) + zip_ref.close() + + +def _install_test_extension3(system=None): # pylint: disable=no-self-use + # We extract the extension into place as we aren't testing install here + zip_file = _get_test_data_file('{}.zip'.format(SECOND_EXT_NAME)) + zip_ref = zipfile.ZipFile(zip_file, 'r') + zip_ref.extractall(build_extension_path(SECOND_EXT_NAME, system=system)) zip_ref.close() @@ -45,12 +54,17 @@ class TestExtensionsBase(unittest.TestCase): def setUp(self): self.ext_dir = tempfile.mkdtemp() - self.patcher = mock.patch('azure.cli.core.extension.EXTENSIONS_DIR', self.ext_dir) - self.patcher.start() + self.ext_sys_dir = tempfile.mkdtemp() + self.patchers = [mock.patch('azure.cli.core.extension.EXTENSIONS_DIR', self.ext_dir), + mock.patch('azure.cli.core.extension.EXTENSIONS_SYS_DIR', self.ext_sys_dir)] + for patcher in self.patchers: + patcher.start() def tearDown(self): - self.patcher.stop() + for patcher in self.patchers: + patcher.stop() shutil.rmtree(self.ext_dir, ignore_errors=True) + shutil.rmtree(self.ext_sys_dir, ignore_errors=True) class TestExtensions(TestExtensionsBase): @@ -238,5 +252,55 @@ def test_wheel_metadata2(self): self.assertTrue(ext.metadata.get(EXT_METADATA_MINCLICOREVERSION)) +class TestWheelSystemExtension(TestExtensionsBase): + + def test_wheel_get_all(self): + _install_test_extension1(system=True) + whl_exts = WheelExtension.get_all() + self.assertEqual(len(whl_exts), 1) + + def test_wheel_user_system_extensions(self): + _install_test_extension1() + _install_test_extension3(system=True) + whl_exts = WheelExtension.get_all() + self.assertEqual(len(whl_exts), 2) + + def test_wheel_user_system_same_extension(self): + _install_test_extension1() + _install_test_extension1(system=True) + self.assertNotEqual(build_extension_path(EXT_NAME), build_extension_path(EXT_NAME, system=True)) + actual = get_extension(EXT_NAME, ext_type=WheelExtension) + self.assertEqual(actual.name, EXT_NAME) + self.assertEqual(actual.path, build_extension_path(EXT_NAME)) + shutil.rmtree(self.ext_dir) + actual = get_extension(EXT_NAME, ext_type=WheelExtension) + self.assertEqual(actual.name, EXT_NAME) + self.assertEqual(actual.path, build_extension_path(EXT_NAME, system=True)) + + def test_wheel_version(self): + _install_test_extension1(system=True) + ext = get_extension(EXT_NAME) + self.assertEqual(ext.version, EXT_VERSION) + + def test_wheel_type(self): + _install_test_extension1(system=True) + ext = get_extension(EXT_NAME) + self.assertEqual(ext.ext_type, 'whl') + + def test_wheel_metadata1(self): + _install_test_extension1(system=True) + ext = get_extension(EXT_NAME) + # There should be no exceptions and metadata should have some value + self.assertTrue(ext.metadata) + + def test_wheel_metadata2(self): + _install_test_extension2(system=True) + ext = get_extension(EXT_NAME) + # There should be no exceptions and metadata should have some value + self.assertTrue(ext.metadata) + # We check that we can retrieve any one of the az extension metadata values + self.assertTrue(ext.metadata.get(EXT_METADATA_MINCLICOREVERSION)) + + if __name__ == '__main__': unittest.main() diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index fed90a8515c..2c220ea574e 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -150,8 +150,8 @@ def _update_latest_from_pypi(versions): return versions, success -def get_az_version_string(): - from azure.cli.core.extension import get_extensions, EXTENSIONS_DIR, DEV_EXTENSION_SOURCES +def get_az_version_string(): # pylint: disable=too-many-statements + from azure.cli.core.extension import get_extensions, EXTENSIONS_DIR, DEV_EXTENSION_SOURCES, EXTENSIONS_SYS_DIR output = six.StringIO() versions = {} @@ -201,6 +201,9 @@ def _get_version_string(name, version_dict): _print() _print("Python location '{}'".format(sys.executable)) _print("Extensions directory '{}'".format(EXTENSIONS_DIR)) + import os + if os.path.isdir(EXTENSIONS_SYS_DIR) and os.listdir(EXTENSIONS_SYS_DIR): + _print("Extensions system directory '{}'".format(EXTENSIONS_SYS_DIR)) if DEV_EXTENSION_SOURCES: _print("Development extension sources:") for source in DEV_EXTENSION_SOURCES: diff --git a/src/azure-cli/azure/cli/command_modules/extension/__init__.py b/src/azure-cli/azure/cli/command_modules/extension/__init__.py index caf43236b54..62ad07d9d5d 100644 --- a/src/azure-cli/azure/cli/command_modules/extension/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/extension/__init__.py @@ -64,6 +64,7 @@ def load_arguments(self, command): c.argument('pip_extra_index_urls', options_list=['--pip-extra-index-urls'], nargs='+', help='Space-separated list of extra URLs of package indexes to use. This should point to a repository compliant with PEP 503 (the simple repository API) or a local directory laid out in the same format.', arg_group='Experimental Pip') c.ignore('_subscription') # hide global subscription param + c.argument('system', action='store_true') with self.argument_context('extension add') as c: c.argument('extension_name', completer=extension_name_from_index_completion_list) diff --git a/src/azure-cli/azure/cli/command_modules/extension/_help.py b/src/azure-cli/azure/cli/command_modules/extension/_help.py index 58298fbef52..1c9284d969b 100644 --- a/src/azure-cli/azure/cli/command_modules/extension/_help.py +++ b/src/azure-cli/azure/cli/command_modules/extension/_help.py @@ -15,6 +15,11 @@ helps['extension add'] = """ type: command short-summary: Add an extension. +parameters: + - name: --system + type: string + short-summary: Use a system directory for the extension. + long-summary: Default path is azure-cli-extensions folder under the CLI running python environment lib path, configurable by environment variable AZURE_EXTENSION_SYS_DIR. On Windows, you may need to open your shell as Administrator to run with the right permission. examples: - name: Add extension by name text: az extension add --name anextension @@ -24,6 +29,8 @@ text: az extension add --source ~/anextension-0.0.1-py2.py3-none-any.whl - name: Add extension from local disk and use pip proxy for dependencies text: az extension add --source ~/anextension-0.0.1-py2.py3-none-any.whl --pip-proxy https://user:pass@proxy.server:8080 + - name: Add extension to system directory + text: az extension add --name anextension --system """ helps['extension list'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/extension/custom.py b/src/azure-cli/azure/cli/command_modules/extension/custom.py index 00e972c130d..51f9e24a095 100644 --- a/src/azure-cli/azure/cli/command_modules/extension/custom.py +++ b/src/azure-cli/azure/cli/command_modules/extension/custom.py @@ -12,9 +12,9 @@ def add_extension_cmd(cmd, source=None, extension_name=None, index_url=None, yes=None, - pip_extra_index_urls=None, pip_proxy=None): + pip_extra_index_urls=None, pip_proxy=None, system=None): return add_extension(cmd=cmd, source=source, extension_name=extension_name, index_url=index_url, yes=yes, - pip_extra_index_urls=pip_extra_index_urls, pip_proxy=pip_proxy) + pip_extra_index_urls=pip_extra_index_urls, pip_proxy=pip_proxy, system=system) def remove_extension_cmd(extension_name):