├── .coveragerc ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ └── Feature_request.md ├── azure-client-tools-bot │ └── config.yml └── pull_request_template.md ├── .gitignore ├── .pylintrc ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── SECURITY.md ├── azdev.pyproj ├── azdev.sln ├── azdev ├── __init__.py ├── __main__.py ├── azdev.completion.sh ├── commands.py ├── completer.py ├── config │ ├── __init__.py │ ├── cli.flake8 │ ├── cli_pylintrc │ ├── ext.flake8 │ └── ext_pylintrc ├── help.py ├── mod_templates │ ├── HISTORY.rst │ ├── README.rst │ ├── _client_factory.py │ ├── _help.py │ ├── _params.py │ ├── _validators.py │ ├── azext_metadata.json │ ├── blank__init__.py │ ├── commands.py │ ├── custom.py │ ├── module__init__.py │ ├── pkg_declare__init__.py │ ├── setup.cfg │ ├── setup.py │ └── test_service_scenario.py ├── operations │ ├── __init__.py │ ├── breaking_change │ │ ├── __init__.py │ │ └── markdown_template.jinja2 │ ├── cmdcov │ │ ├── __init__.py │ │ ├── _macros.j2 │ │ ├── cmdcov.py │ │ ├── component.css │ │ ├── component.js │ │ ├── favicon.ico │ │ ├── index.j2 │ │ ├── index2.j2 │ │ └── module.j2 │ ├── code_gen.py │ ├── command_change │ │ ├── __init__.py │ │ ├── custom.py │ │ └── util.py │ ├── constant.py │ ├── extensions │ │ ├── __init__.py │ │ ├── util.py │ │ └── version_upgrade.py │ ├── help │ │ ├── __init__.py │ │ └── refdoc │ │ │ ├── __init__.py │ │ │ ├── cli_docs │ │ │ ├── __init__.py │ │ │ ├── helpgen.py │ │ │ └── ind.rst │ │ │ ├── common │ │ │ ├── __init__.py │ │ │ └── directives.py │ │ │ ├── conf.py │ │ │ └── extension_docs │ │ │ ├── __init__.py │ │ │ ├── helpgen.py │ │ │ └── ind.rst │ ├── legal.py │ ├── linter │ │ ├── __init__.py │ │ ├── data │ │ │ └── cmd_example_config.json │ │ ├── linter.py │ │ ├── pylint_checkers │ │ │ ├── __init__.py │ │ │ └── show_command.py │ │ ├── rule_decorators.py │ │ ├── rules │ │ │ ├── __init__.py │ │ │ ├── ci_exclusions.yml │ │ │ ├── command_coverage_rules.py │ │ │ ├── command_group_rules.py │ │ │ ├── command_rules.py │ │ │ ├── help_rules.py │ │ │ ├── linter_exclusions.yml │ │ │ └── parameter_rules.py │ │ └── util.py │ ├── performance.py │ ├── pypi.py │ ├── python_sdk.py │ ├── regex.py │ ├── resource.py │ ├── secret.py │ ├── setup.py │ ├── statistics │ │ ├── __init__.py │ │ └── util.py │ ├── style.py │ ├── tests │ │ ├── __init__.py │ │ ├── files │ │ │ ├── email_string.txt │ │ │ ├── simple_string.txt │ │ │ └── subdir │ │ │ │ └── info.json │ │ ├── jsons │ │ │ ├── az_ams_meta_after.json │ │ │ ├── az_ams_meta_before.json │ │ │ ├── az_costmanagement_meta_after.json │ │ │ ├── az_costmanagement_meta_before.json │ │ │ ├── az_monitor_meta_after.json │ │ │ └── az_monitor_meta_before.json │ │ ├── test_benchmark.py │ │ ├── test_break_change.py │ │ ├── test_break_change_ci.sh │ │ ├── test_cmdcov.py │ │ ├── test_config.py │ │ ├── test_extension_versioning.py │ │ ├── test_scan_and_mask.py │ │ └── test_style.py │ └── testtool │ │ ├── __init__.py │ │ ├── incremental_strategy.py │ │ ├── profile_context.py │ │ ├── pytest_runner.py │ │ └── tests │ │ ├── __init__.py │ │ └── test_profile_context.py ├── params.py ├── transformers.py └── utilities │ ├── __init__.py │ ├── command.py │ ├── config.py │ ├── const.py │ ├── display.py │ ├── git_util.py │ ├── path.py │ ├── pypi.py │ ├── testing.py │ ├── tests │ ├── __init__.py │ └── test_path.py │ └── tools.py ├── azure-cli-diff-tool ├── HISTORY.rst ├── README.rst ├── azure_cli_diff_tool │ ├── __init__.py │ ├── _const.py │ ├── data │ │ ├── blob_config.ini │ │ └── meta_change_whitelist.txt │ ├── meta_change.py │ ├── meta_change_detect.py │ └── utils.py ├── pyproject.toml ├── setup.py └── tests │ ├── __init__.py │ ├── jsons │ ├── az_acr_meta_after.json │ ├── az_acr_meta_before.json │ ├── az_ams_meta_after.json │ ├── az_ams_meta_before.json │ ├── az_monitor_meta_after.json │ ├── az_monitor_meta_before.json │ ├── az_mysql_meta_after.json │ ├── az_mysql_meta_before.json │ ├── az_network-manager_meta_after.json │ └── az_virtual-network-manager_meta_before.json │ └── test_break_change.py ├── azure-pipelines-cli.yml ├── azure-pipelines.yml ├── pyproject.toml ├── pytestdebug.log ├── scripts ├── ci │ ├── build.sh │ ├── build_cli_diff_tool.sh │ ├── extract_version.sh │ ├── install.sh │ └── run_tox.sh └── license_verify.py ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = */tests/* 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 10 4 | ignore = 5 | E126, 6 | E501, 7 | F401, 8 | F811, 9 | C901, 10 | W503, 11 | W504 12 | per-file-ignores = 13 | azdev/help.py:W605 14 | azdev/operations/tests/test_scan_and_mask.py:W605 15 | 16 | exclude = 17 | mod_templates 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | -------------------------------------------------------------------------------- /.github/azure-client-tools-bot/config.yml: -------------------------------------------------------------------------------- 1 | files_check: 2 | - files: 3 | - "HISTORY.rst" 4 | - "azdev/__init__.py" 5 | comment: | 6 | Please write the description of change into HISTORY.rst. 7 | If you want to release a new azdev version, please update the __VERSION__ in __init__.py as well. 8 | is_regex: true 9 | type: necessary 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Related command** 2 | 3 | 4 | **Description** 5 | 6 | 7 | 8 | --- 9 | 10 | This checklist is used to make sure that common guidelines for a pull request are followed. 11 | 12 | - [ ] `pylint azdev --rcfile=.pylintrc -r n` 13 | 14 | - [ ] `flake8 --statistics --append-config=.flake8 azdev` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | .vs/ 3 | src/aztool.egg-info/ 4 | src/aztool/__pycache__/__init__.cpython-36.pyc 5 | src/aztool/__pycache__/ 6 | src/azdev.egg-info/* 7 | *.pyc 8 | .vscode/ 9 | azdev.egg-info/ 10 | .tox/ 11 | *.idea 12 | pip-wheel-metadata/* 13 | build/ 14 | dist/ 15 | cmd_coverage 16 | tested_command.txt 17 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | # For all codes, run 'pylint --list-msgs' or go to 'http://pylint-messages.wikidot.com/all-codes' 3 | disable= 4 | cyclic-import, 5 | fixme, 6 | import-outside-toplevel, 7 | invalid-name, 8 | missing-docstring, 9 | raise-missing-from, 10 | too-few-public-methods, 11 | too-many-arguments, 12 | too-many-positional-arguments, 13 | consider-using-f-string, 14 | unspecified-encoding, 15 | # These rules were added in Pylint >= 2.12, disable them to avoid making retroactive change 16 | broad-exception-raised, 17 | missing-timeout 18 | 19 | [TYPECHECK] 20 | # For Azure CLI extensions, we ignore some import errors as they'll be available in the environment of the CLI 21 | ignored-modules=azure,azure.cli,azure.cli.core,knack 22 | 23 | [FORMAT] 24 | max-line-length=120 25 | 26 | [DESIGN] 27 | # Maximum number of locals for function / method body 28 | max-locals=25 29 | # Maximum number of branch for function / method body 30 | max-branches=20 31 | 32 | [SIMILARITIES] 33 | min-similarity-lines=10 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include HISTORY.rst 3 | include README.md 4 | recursive-include azdev/config * 5 | recursive-include azdev/mod_templates * 6 | recursive-include azdev/operations/linter/rules * 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Microsoft Azure CLI Dev Tools (azdev) 2 | ===================================== 3 | 4 | The ``azdev`` tool is designed to aid new and experienced developers in contributing to Azure CLI command modules and extensions. 5 | 6 | Notes: `azdev` command line tool is only designed for internal use and running on a local machine. It should never be used to take input from untrusted/outside sources or used behind another application. 7 | 8 | Setting up your development environment 9 | +++++++++++++++++++++++++++++++++++++++ 10 | 11 | 1. Install Python 3.6+ from http://python.org. Please note that the version of Python that comes preinstalled on OSX is 2.7. 12 | 13 | 2. Fork and clone the repository or repositories you wish to develop for. 14 | - For Azure CLI: https://github.com/Azure/azure-cli 15 | - For Azure CLI Extensions: https://github.com/Azure/azure-cli-extensions 16 | - Any other repository that you might have access to that contains CLI extensions. 17 | 18 | 3. Create a new virtual environment for Python in the root of your clone. You can do this by running: 19 | 20 | Python 3.6+ (all platforms): 21 | 22 | :: 23 | 24 | python -m venv env 25 | 26 | or: 27 | 28 | :: 29 | 30 | python3 -m venv env 31 | 32 | 33 | 4. Activate the env virtual environment by running: 34 | 35 | Windows CMD.exe: 36 | 37 | :: 38 | 39 | env\scripts\activate.bat 40 | 41 | Windows Powershell: 42 | 43 | :: 44 | 45 | env\scripts\activate.ps1 46 | 47 | 48 | OSX/Linux (bash): 49 | 50 | :: 51 | 52 | source env/bin/activate 53 | 54 | 5. Install ``azdev`` by running: 55 | 56 | :: 57 | 58 | pip install azdev 59 | 60 | 6. Complete setup by running: 61 | 62 | :: 63 | 64 | azdev setup 65 | 66 | 67 | This will launch the interactive setup process. To see non-interactive options run `azdev setup -h`. 68 | 69 | Reporting issues and feedback 70 | +++++++++++++++++++++++++++++ 71 | 72 | If you encounter any bugs with the tool please file an issue in the `Issues `__ section of our GitHub repo. 73 | 74 | Contribute Code 75 | +++++++++++++++ 76 | 77 | This project has adopted the `Microsoft Open Source Code of Conduct `__. 78 | 79 | For more information see the `Code of Conduct FAQ `__ or contact `opencode@microsoft.com `__ with any additional questions or comments. 80 | 81 | If you would like to become an active contributor to this project please 82 | follow the instructions provided in `Microsoft Azure Projects Contribution Guidelines `__. 83 | 84 | License 85 | +++++++ 86 | 87 | :: 88 | 89 | Azure CLI Dev Tools (azdev) 90 | 91 | Copyright (c) Microsoft Corporation 92 | All rights reserved. 93 | 94 | MIT License 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 97 | 98 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.:: 101 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azdev.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | {6285073d-e427-48c1-9783-abddf33fa22c} 7 | 8 | azdev\__init__.py 9 | 10 | . 11 | . 12 | {888888a0-9f3d-457c-b088-3a5042f75d52} 13 | Standard Python launcher 14 | MSBuild|env|$(MSBuildProjectFullPath) 15 | test -h 16 | False 17 | 18 | 19 | 20 | 21 | 10.0 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | 69 | 70 | Code 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | env 88 | 3.6 89 | env (Python 3.6 (32-bit)) 90 | Scripts\python.exe 91 | Scripts\pythonw.exe 92 | PYTHONPATH 93 | X86 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /azdev.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2016 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "azdev", "azdev.pyproj", "{6285073D-E427-48C1-9783-ABDDF33FA22C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6285073D-E427-48C1-9783-ABDDF33FA22C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6285073D-E427-48C1-9783-ABDDF33FA22C}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ExtensibilityGlobals) = postSolution 21 | SolutionGuid = {B538A664-C4F4-40F0-9BE6-321562C2AA09} 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /azdev/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | __VERSION__ = '0.2.4' 8 | -------------------------------------------------------------------------------- /azdev/__main__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import sys 8 | 9 | from knack import CLI, CLICommandsLoader 10 | 11 | from azdev.help import helps # pylint: disable=unused-import 12 | from azdev.utilities import get_azdev_config_dir 13 | 14 | 15 | class AzDevCli(CLI): 16 | 17 | def get_cli_version(self): 18 | from azdev import __VERSION__ 19 | return __VERSION__ 20 | 21 | 22 | class AzDevCommandsLoader(CLICommandsLoader): 23 | def load_command_table(self, args): 24 | from azdev.commands import load_command_table 25 | 26 | load_command_table(self, args) 27 | return super().load_command_table(args) 28 | 29 | def load_arguments(self, command): 30 | from azdev.params import load_arguments 31 | 32 | load_arguments(self, command) 33 | super().load_arguments(command) 34 | 35 | 36 | def main(): 37 | try: 38 | azdev = AzDevCli(cli_name='azdev', commands_loader_cls=AzDevCommandsLoader, 39 | config_dir=get_azdev_config_dir()) 40 | exit_code = azdev.invoke(sys.argv[1:]) 41 | sys.exit(exit_code) 42 | except KeyboardInterrupt: 43 | sys.exit(1) 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | -------------------------------------------------------------------------------- /azdev/azdev.completion.sh: -------------------------------------------------------------------------------- 1 | case $SHELL in 2 | */zsh) 3 | echo 'Enabling ZSH compatibility mode'; 4 | autoload bashcompinit && bashcompinit 5 | ;; 6 | */bash) 7 | ;; 8 | *) 9 | esac 10 | 11 | eval "$(register-python-argcomplete azdev)" 12 | -------------------------------------------------------------------------------- /azdev/commands.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | from knack.commands import CommandGroup 8 | 9 | from .transformers import performance_benchmark_data_transformer 10 | 11 | 12 | # pylint: disable=too-many-statements 13 | def load_command_table(self, _): 14 | 15 | def operation_group(name): 16 | return 'azdev.operations.{}#{{}}'.format(name) 17 | 18 | with CommandGroup(self, '', operation_group('setup')) as g: 19 | g.command('setup', 'setup') 20 | 21 | # TODO: enhance with tox support 22 | with CommandGroup(self, '', operation_group('testtool')) as g: 23 | g.command('test', 'run_tests') 24 | 25 | with CommandGroup(self, '', operation_group('style')) as g: 26 | g.command('style', 'check_style') 27 | 28 | with CommandGroup(self, '', operation_group('linter')) as g: 29 | g.command('linter', 'run_linter') 30 | 31 | with CommandGroup(self, '', operation_group('secret')) as g: 32 | g.command('scan', 'scan_secrets') 33 | g.command('mask', 'mask_secrets') 34 | 35 | with CommandGroup(self, 'statistics', operation_group('statistics')) as g: 36 | g.command('list-command-table', 'list_command_table') 37 | g.command('diff-command-tables', 'diff_command_tables') 38 | 39 | with CommandGroup(self, '', operation_group('cmdcov')) as g: 40 | g.command('cmdcov', 'run_cmdcov') 41 | 42 | with CommandGroup(self, 'verify', operation_group('pypi')) as g: 43 | g.command('history', 'check_history') 44 | 45 | with CommandGroup(self, 'command-change', operation_group('command_change')) as g: 46 | g.command('meta-export', 'export_command_meta') 47 | g.command('meta-diff', 'cmp_command_meta') 48 | g.command('tree-export', 'export_command_tree') 49 | 50 | with CommandGroup(self, 'cli', operation_group('pypi')) as g: 51 | g.command('check-versions', 'verify_versions') 52 | 53 | with CommandGroup(self, '', operation_group('code_gen')) as g: 54 | g.command('cli create', 'create_module') 55 | g.command('extension create', 'create_extension') 56 | 57 | with CommandGroup(self, 'verify', operation_group('help')) as g: 58 | g.command('document-map', 'check_document_map') 59 | 60 | with CommandGroup(self, 'verify', operation_group('legal')) as g: 61 | g.command('license', 'check_license_headers') 62 | 63 | with CommandGroup(self, 'perf', operation_group('performance')) as g: 64 | g.command('load-times', 'check_load_time') 65 | g.command('benchmark', 'benchmark', is_preview=True, table_transformer=performance_benchmark_data_transformer) 66 | 67 | with CommandGroup(self, 'extension', operation_group('extensions')) as g: 68 | g.command('add', 'add_extension') 69 | g.command('remove', 'remove_extension') 70 | g.command('list', 'list_extensions') 71 | g.command('build', 'build_extensions') 72 | g.command('publish', 'publish_extensions') 73 | g.command('update-index', 'update_extension_index') 74 | g.command('cal-next-version', 'cal_next_version') 75 | g.command('show', 'show_extension') 76 | 77 | with CommandGroup(self, 'extension repo', operation_group('extensions')) as g: 78 | g.command('add', 'add_extension_repo') 79 | g.command('remove', 'remove_extension_repo') 80 | g.command('list', 'list_extension_repos') 81 | 82 | with CommandGroup(self, 'cli', operation_group('help')) as g: 83 | g.command('generate-docs', 'generate_cli_ref_docs') 84 | 85 | with CommandGroup(self, 'extension', operation_group('help')) as g: 86 | g.command('generate-docs', 'generate_extension_ref_docs') 87 | 88 | with CommandGroup(self, '', operation_group('breaking_change')) as g: 89 | g.command('generate-breaking-change-report', 'collect_upcoming_breaking_changes') 90 | -------------------------------------------------------------------------------- /azdev/completer.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | # TODO: import from Knack once it is moved 9 | # pylint: disable=too-few-public-methods 10 | class Completer: 11 | 12 | def __init__(self, func): 13 | self.func = func 14 | 15 | def __call__(self, **kwargs): 16 | namespace = kwargs['parsed_args'] 17 | prefix = kwargs['prefix'] 18 | cmd = namespace._cmd # pylint: disable=protected-access 19 | return self.func(cmd, prefix, namespace) 20 | 21 | 22 | @Completer 23 | def get_test_completion(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument 24 | # TODO: return the list of keys from the index 25 | return ['storage', 'network', 'redis'] 26 | -------------------------------------------------------------------------------- /azdev/config/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/config/cli.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 10 4 | ignore = 5 | # line too long, it is covered by pylint 6 | E501, 7 | # bare except, bad practice, to be removed in the future 8 | E722, 9 | # imported but unused, too many violations, to be removed in the future 10 | F401, 11 | # redefinition of unused, to be removed in the future 12 | F811, 13 | # code flow is too complex, too many violations, to be removed in the future 14 | C901, 15 | # line break before binary operator 16 | W503, 17 | # line break after binary operator effect on readability is subjective 18 | W504 19 | exclude = 20 | azure_cli_bdist_wheel.py 21 | build 22 | tools 23 | scripts 24 | doc 25 | build_scripts 26 | */grammar/ 27 | -------------------------------------------------------------------------------- /azdev/config/cli_pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore-patterns=test_* 3 | reports=no 4 | 5 | [MESSAGES CONTROL] 6 | # For all codes, run 'pylint --list-msgs' or go to 'https://pylint.readthedocs.io/en/latest/reference_guide/features.html' 7 | # locally-disabled: Warning locally suppressed using disable-msg 8 | # cyclic-import: because of https://github.com/PyCQA/pylint/issues/850 9 | # too-many-arguments: Due to the nature of the CLI many commands have large arguments set which reflect in large arguments set in corresponding methods. 10 | # useless-object-inheritance: Specifies it can be removed from python3 bases, but we also support Python 2.7 11 | # chained-comparison: Does not provide guidance on what would be better and arguably these are fine. 12 | # useless-import-alias: Removing the alias often breaks the import, so it isn't really useless after all. 13 | # useless-suppression: Depends on the Python version and the pylint version... 14 | # wrong-import-order: Pylint keeps changing its mind... 15 | disable= 16 | missing-docstring, 17 | locally-disabled, 18 | fixme, 19 | cyclic-import, 20 | too-many-arguments, 21 | invalid-name, 22 | duplicate-code, 23 | useless-object-inheritance, 24 | chained-comparison, 25 | useless-import-alias, 26 | useless-suppression, 27 | import-outside-toplevel, 28 | wrong-import-order, 29 | # These rules were added in Pylint >= 2.12, disable them to avoid making retroactive change 30 | broad-exception-raised, 31 | missing-timeout 32 | 33 | [FORMAT] 34 | max-line-length=120 35 | 36 | [VARIABLES] 37 | # Tells whether we should check for unused import in __init__ files. 38 | init-import=yes 39 | 40 | [DESIGN] 41 | # Maximum number of locals for function / method body 42 | max-locals=25 43 | # Maximum number of branch for function / method body 44 | max-branches=20 45 | 46 | [SIMILARITIES] 47 | min-similarity-lines=10 48 | -------------------------------------------------------------------------------- /azdev/config/ext.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 10 4 | ignore = 5 | # line too long, it is covered by pylint 6 | E501, 7 | # bare except, bad practice, to be removed in the future 8 | E722, 9 | # imported but unused, too many violations, to be removed in the future 10 | F401, 11 | # redefinition of unused, to be removed in the future 12 | F811, 13 | # code flow is too complex, too many violations, to be removed in the future 14 | C901, 15 | # line break before binary operator 16 | W503, 17 | # line break after binary operator effect on readability is subjective 18 | W504 19 | exclude = 20 | */vendored_sdks 21 | docs 22 | scripts 23 | -------------------------------------------------------------------------------- /azdev/config/ext_pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore=tests,generated,vendored_sdks,privates 3 | reports=no 4 | 5 | [MESSAGES CONTROL] 6 | # For all codes, run 'pylint --list-msgs' or go to 'https://pylint.readthedocs.io/en/latest/reference_guide/features.html' 7 | # locally-disabled: Warning locally suppressed using disable-msg 8 | # cyclic-import: Because of https://github.com/PyCQA/pylint/issues/850 9 | # too-many-arguments: Due to the nature of the CLI many commands have large arguments set which reflect in large arguments set in corresponding methods. 10 | # import-outside-toplevel: Lazy import to improve performance 11 | disable= 12 | missing-docstring, 13 | locally-disabled, 14 | fixme, 15 | cyclic-import, 16 | too-many-arguments, 17 | invalid-name, 18 | duplicate-code, 19 | import-outside-toplevel, 20 | # These rules were added in Pylint >= 2.12, disable them to avoid making retroactive change 21 | broad-exception-raised, 22 | missing-timeout 23 | 24 | [TYPECHECK] 25 | # For Azure CLI extensions, we ignore some import errors as they'll be available in the environment of the CLI 26 | ignored-modules=azure,azure.cli,azure.cli.core,azure.cli.core.commands,knack,msrestazure,argcomplete,azure_devtools,isodate,OpenSSL 27 | 28 | [FORMAT] 29 | max-line-length=120 30 | 31 | [VARIABLES] 32 | # Tells whether we should check for unused import in __init__ files. 33 | init-import=yes 34 | 35 | [DESIGN] 36 | # Maximum number of locals for function / method body 37 | max-locals=25 38 | # Maximum number of branch for function / method body 39 | max-branches=20 40 | 41 | [SIMILARITIES] 42 | min-similarity-lines=10 43 | -------------------------------------------------------------------------------- /azdev/mod_templates/HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Release History 4 | =============== 5 | 6 | 0.1.0 7 | ++++++ 8 | * Initial release. 9 | -------------------------------------------------------------------------------- /azdev/mod_templates/README.rst: -------------------------------------------------------------------------------- 1 | Microsoft Azure CLI '{{ name }}' {{ 'Extension' if is_ext else 'Command Module' }} 2 | ========================================== 3 | 4 | This package is for the '{{ name }}' {{ 'extension' if is_ext else 'module' }}. 5 | i.e. 'az {{ name }}' 6 | -------------------------------------------------------------------------------- /azdev/mod_templates/_client_factory.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | def cf_{{ name }}(cli_ctx, *_): 7 | {% if client_name %} 8 | from azure.cli.core.commands.client_factory import get_mgmt_service_client 9 | from {{ sdk_path }} import {{ client_name }} 10 | return get_mgmt_service_client(cli_ctx, {{ client_name }}) 11 | {% else %} 12 | from azure.cli.core.commands.client_factory import get_mgmt_service_client 13 | # TODO: Replace CONTOSO with the appropriate label and uncomment 14 | # from azure.mgmt.CONTOSO import CONTOSOManagementClient 15 | # return get_mgmt_service_client(cli_ctx, CONTOSOManagementClient) 16 | return None 17 | {% endif %} -------------------------------------------------------------------------------- /azdev/mod_templates/_help.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # -------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See License.txt in the project root for license information. 5 | # -------------------------------------------------------------------------------------------- 6 | 7 | from knack.help_files import helps # pylint: disable=unused-import 8 | 9 | 10 | helps['{{ name }}'] = """ 11 | type: group 12 | short-summary: Commands to manage {{ display_name_plural }}. 13 | """ 14 | 15 | helps['{{ name }} create'] = """ 16 | type: command 17 | short-summary: Create a {{ display_name }}. 18 | """ 19 | 20 | helps['{{ name }} list'] = """ 21 | type: command 22 | short-summary: List {{ display_name_plural }}. 23 | """ 24 | {% if sdk_path %} 25 | helps['{{ name }} delete'] = """ 26 | type: command 27 | short-summary: Delete a {{ display_name }}. 28 | """ 29 | 30 | helps['{{ name }} show'] = """ 31 | type: command 32 | short-summary: Show details of a {{ display_name }}. 33 | """ 34 | 35 | helps['{{ name }} update'] = """ 36 | type: command 37 | short-summary: Update a {{ display_name }}. 38 | """ 39 | {% else %} 40 | # helps['{{ name }} delete'] = """ 41 | # type: command 42 | # short-summary: Delete a {{ display_name }}. 43 | # """ 44 | 45 | # helps['{{ name }} show'] = """ 46 | # type: command 47 | # short-summary: Show details of a {{ display_name }}. 48 | # """ 49 | 50 | # helps['{{ name }} update'] = """ 51 | # type: command 52 | # short-summary: Update a {{ display_name }}. 53 | # """ 54 | {% endif %} -------------------------------------------------------------------------------- /azdev/mod_templates/_params.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | # pylint: disable=line-too-long 6 | 7 | from knack.arguments import CLIArgumentType 8 | 9 | 10 | def load_arguments(self, _): 11 | 12 | from azure.cli.core.commands.parameters import tags_type 13 | from azure.cli.core.commands.validators import get_default_location_from_resource_group 14 | 15 | {{ sdk_property }}_type = CLIArgumentType(options_list='--{{ sdk_property.replace("_", "-") }}-name', help='Name of the {{ display_name }}.', id_part='name') 16 | 17 | with self.argument_context('{{ name }}') as c: 18 | c.argument('tags', tags_type) 19 | c.argument('location', validator=get_default_location_from_resource_group) 20 | c.argument('{{ sdk_property }}', {{ sdk_property }}_type, options_list=['--name', '-n']) 21 | 22 | with self.argument_context('{{ name }} list') as c: 23 | c.argument('{{ sdk_property }}', {{ sdk_property }}_type, id_part=None) 24 | 25 | -------------------------------------------------------------------------------- /azdev/mod_templates/_validators.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | 7 | def example_name_or_id_validator(cmd, namespace): 8 | # Example of a storage account name or ID validator. 9 | # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters 10 | from azure.cli.core.commands.client_factory import get_subscription_id 11 | from msrestazure.tools import is_valid_resource_id, resource_id 12 | if namespace.storage_account: 13 | if not is_valid_resource_id(namespace.RESOURCE): 14 | namespace.storage_account = resource_id( 15 | subscription=get_subscription_id(cmd.cli_ctx), 16 | resource_group=namespace.resource_group_name, 17 | namespace='Microsoft.Storage', 18 | type='storageAccounts', 19 | name=namespace.storage_account 20 | ) 21 | 22 | -------------------------------------------------------------------------------- /azdev/mod_templates/azext_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | {% if is_preview %}"azext.isPreview": true,{% endif %} 3 | "azext.minCliCoreVersion": "2.0.67", 4 | "azext.maxCliCoreVersion": "2.1.0" 5 | } -------------------------------------------------------------------------------- /azdev/mod_templates/blank__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/mod_templates/commands.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | # pylint: disable=line-too-long 7 | from azure.cli.core.commands import CliCommandType 8 | from {{ mod_path }}._client_factory import cf_{{ name }} 9 | 10 | 11 | def load_command_table(self, _): 12 | {% if sdk_path %} 13 | {{ name }}_sdk = CliCommandType( 14 | operations_tmpl='{{ sdk_path }}.operations#{{ operation_name }}.{}', 15 | client_factory=cf_{{ name }}) 16 | {% else %} 17 | # TODO: Add command type here 18 | # {{ name }}_sdk = CliCommandType( 19 | # operations_tmpl='.operations#{{ operation_name }}.{}', 20 | # client_factory=cf_{{ name }}) 21 | {% endif %} 22 | {% if sdk_path %} 23 | with self.command_group('{{ name }}', {{ name }}_sdk, client_factory=cf_{{ name }}) as g: 24 | g.custom_command('create', 'create_{{ name }}') 25 | g.command('delete', 'delete') 26 | g.custom_command('list', 'list_{{ name }}') 27 | g.show_command('show', 'get') 28 | g.generic_update_command('update', setter_name='update', custom_func_name='update_{{ name }}') 29 | {% else %} 30 | with self.command_group('{{ name }}') as g: 31 | g.custom_command('create', 'create_{{ name }}') 32 | # g.command('delete', 'delete') 33 | g.custom_command('list', 'list_{{ name }}') 34 | # g.show_command('show', 'get') 35 | # g.generic_update_command('update', setter_name='update', custom_func_name='update_{{ name }}') 36 | {% endif %} 37 | {% if is_preview %} 38 | with self.command_group('{{ name }}', is_preview=True): 39 | pass 40 | {% endif %} 41 | 42 | -------------------------------------------------------------------------------- /azdev/mod_templates/custom.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | from knack.util import CLIError 7 | 8 | 9 | def create_{{ name }}(cmd, {% if sdk_path %}client, {% endif %}resource_group_name, {% if sdk_property %}{{sdk_property}}, {% else %} { name }}_name, {% endif %}location=None, tags=None): 10 | raise CLIError('TODO: Implement `{{ name }} create`') 11 | 12 | 13 | def list_{{ name }}(cmd, {% if sdk_path %}client, {% endif %}resource_group_name=None): 14 | raise CLIError('TODO: Implement `{{ name }} list`') 15 | 16 | 17 | def update_{{ name }}(cmd, instance, tags=None): 18 | with cmd.update_context(instance) as c: 19 | c.set_param('tags', tags) 20 | return instance 21 | -------------------------------------------------------------------------------- /azdev/mod_templates/module__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | from azure.cli.core import AzCommandsLoader 7 | 8 | from {{ mod_path }}._help import helps # pylint: disable=unused-import 9 | 10 | 11 | class {{ loader_name }}(AzCommandsLoader): 12 | 13 | def __init__(self, cli_ctx=None): 14 | from azure.cli.core.commands import CliCommandType 15 | from {{ mod_path }}._client_factory import cf_{{ name }} 16 | {{ name }}_custom = CliCommandType( 17 | operations_tmpl='{{ mod_path }}.custom#{}', 18 | client_factory=cf_{{ name }}) 19 | super({{ loader_name }}, self).__init__(cli_ctx=cli_ctx, 20 | custom_command_type={{ name }}_custom) 21 | 22 | def load_command_table(self, args): 23 | from {{ mod_path }}.commands import load_command_table 24 | load_command_table(self, args) 25 | return self.command_table 26 | 27 | def load_arguments(self, command): 28 | from {{ mod_path }}._params import load_arguments 29 | load_arguments(self, command) 30 | 31 | 32 | COMMAND_LOADER_CLS = {{ loader_name }} 33 | 34 | -------------------------------------------------------------------------------- /azdev/mod_templates/pkg_declare__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | import pkg_resources 7 | pkg_resources.declare_namespace(__name__) 8 | -------------------------------------------------------------------------------- /azdev/mod_templates/setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-cli-dev-tools/5130a24020ca0a47055e914a58a4af67d9eb98ad/azdev/mod_templates/setup.cfg -------------------------------------------------------------------------------- /azdev/mod_templates/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -------------------------------------------------------------------------------------------- 4 | # Copyright (c) Microsoft Corporation. All rights reserved. 5 | # Licensed under the MIT License. See License.txt in the project root for license information. 6 | # -------------------------------------------------------------------------------------------- 7 | 8 | 9 | from codecs import open 10 | from setuptools import setup, find_packages 11 | try: 12 | from azure_bdist_wheel import cmdclass 13 | except ImportError: 14 | from distutils import log as logger 15 | logger.warn("Wheel is not available, disabling bdist_wheel hook") 16 | 17 | # TODO: Confirm this is the right version number you want and it matches your 18 | # HISTORY.rst entry. 19 | VERSION = '0.1.0' 20 | 21 | # The full list of classifiers is available at 22 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 23 | CLASSIFIERS = [ 24 | 'Development Status :: 4 - Beta', 25 | 'Intended Audience :: Developers', 26 | 'Intended Audience :: System Administrators', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.6', 30 | 'Programming Language :: Python :: 3.7', 31 | 'Programming Language :: Python :: 3.8', 32 | 'License :: OSI Approved :: MIT License', 33 | ] 34 | 35 | # TODO: Add any additional SDK dependencies here 36 | DEPENDENCIES = {% if dependencies %}[ 37 | {{ dependencies|join(',\n '|safe) }} 38 | ]{% else %}[]{% endif %} 39 | 40 | with open('README.rst', 'r', encoding='utf-8') as f: 41 | README = f.read() 42 | with open('HISTORY.rst', 'r', encoding='utf-8') as f: 43 | HISTORY = f.read() 44 | 45 | setup( 46 | name='{{ pkg_name }}', 47 | version=VERSION, 48 | description='Microsoft Azure Command-Line Tools {{ display_name }} {{ 'Extension' if is_ext else 'Command Module' }}', 49 | # TODO: Update author and email, if applicable 50 | author='Microsoft Corporation', 51 | author_email='azpycli@microsoft.com', 52 | # TODO: change to your extension source code repo if the code will not be put in azure-cli-extensions repo 53 | url='https://github.com/Azure/{% if is_ext %}azure-cli-extensions/tree/master/src/{{ pkg_name }}{% else %}azure-cli{% endif %}', 54 | long_description=README + '\n\n' + HISTORY, 55 | license='MIT', 56 | classifiers=CLASSIFIERS, 57 | packages=find_packages(), 58 | install_requires=DEPENDENCIES, 59 | {% if is_ext %} package_data={'{{ ext_long_name }}': ['azext_metadata.json']},{% endif %} 60 | ) 61 | -------------------------------------------------------------------------------- /azdev/mod_templates/test_service_scenario.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | import os 7 | import unittest 8 | 9 | from azure_devtools.scenario_tests import AllowLargeResponse 10 | from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) 11 | 12 | 13 | TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) 14 | 15 | 16 | class {{ name.capitalize() }}ScenarioTest(ScenarioTest): 17 | 18 | @ResourceGroupPreparer(name_prefix='cli_test_{{ name }}') 19 | def test_{{ name }}(self, resource_group): 20 | 21 | self.kwargs.update({ 22 | 'name': 'test1' 23 | }) 24 | 25 | self.cmd('{{ name }} create -g {rg} -n {name} --tags foo=doo', checks=[ 26 | self.check('tags.foo', 'doo'), 27 | self.check('name', '{name}') 28 | ]) 29 | self.cmd('{{ name }} update -g {rg} -n {name} --tags foo=boo', checks=[ 30 | self.check('tags.foo', 'boo') 31 | ]) 32 | count = len(self.cmd('{{ name }} list').get_output_in_json()) 33 | self.cmd('{{ name }} show - {rg} -n {name}', checks=[ 34 | self.check('name', '{name}'), 35 | self.check('resourceGroup', '{rg}'), 36 | self.check('tags.foo', 'boo') 37 | ]) 38 | self.cmd('{{ name }} delete -g {rg} -n {name}') 39 | final_count = len(self.cmd('{{ name }} list').get_output_in_json()) 40 | self.assertTrue(final_count, count - 1) 41 | -------------------------------------------------------------------------------- /azdev/operations/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/operations/breaking_change/markdown_template.jinja2: -------------------------------------------------------------------------------- 1 | {% if not no_head -%} 2 | # Upcoming breaking changes in Azure CLI 3 | 4 | The breaking changes listed in this article are planned for the next major release of the Azure CLI unless otherwise noted. Per our [Support lifecycle](./azure-cli-support-lifecycle.md), breaking changes in Azure CLI Core reference groups occur twice a year. 5 | 6 | {% endif -%} 7 | {% for module, command_bc in module_bc.items() -%} 8 | ## {{ module }} 9 | 10 | {% for command, multi_version_bcs in command_bc.items() -%} 11 | {% if not module == 'core' -%} 12 | ### `{{ command }}` 13 | 14 | {% endif -%} 15 | {% if multi_version_bcs.group_ref -%} 16 | [Link to {{ multi_version_bcs.group_ref|join(' ') }} reference group](/cli/azure/{{ multi_version_bcs.group_ref|join('/') }}) 17 | 18 | {% endif -%} 19 | {% if multi_version_bcs['items'] is mapping -%} 20 | {% for version, bcs in multi_version_bcs['items'] | dictsort -%} 21 | ###{%- if not module == 'core' -%}#{%- endif %} Deprecated in {{ version }} 22 | 23 | {% for bc in bcs -%} 24 | {{ bc }} 25 | {% endfor %} 26 | 27 | {% endfor -%} 28 | {% else -%} 29 | 30 | {% for bc in multi_version_bcs['items'] -%} 31 | {{ bc }} 32 | {% endfor %} 33 | 34 | {% endif -%} 35 | {% endfor -%} 36 | {% endfor -%} 37 | {% if not no_tail %} 38 | > [!NOTE] 39 | > This article provides information on upcoming breaking changes. For previously published breaking changes, see [Azure CLI release notes](./release-notes-azure-cli.md). 40 | {%- endif -%} -------------------------------------------------------------------------------- /azdev/operations/cmdcov/_macros.j2: -------------------------------------------------------------------------------- 1 | {% macro render_percentage(detail, module) %} 2 | {% if module['color'] == 'N/A' -%} 3 | 4 |
N/A
5 |
Not applicable
6 | 7 | {% elif module['color'] != 'gold' -%} 8 | 9 |
10 | 11 | 16 | 22 | {{ module['percentage'] }}% 23 | 24 |
{{ detail }}
25 |
26 | 27 | {% else -%} 28 | 29 |
30 |
31 |
100.000%
32 | 33 | {% endif -%} 34 | {% endmacro %} 35 | 36 | -------------------------------------------------------------------------------- /azdev/operations/cmdcov/component.css: -------------------------------------------------------------------------------- 1 | *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } 2 | body { 3 | font-family: sans-serif; 4 | } 5 | 6 | /* Container styles */ 7 | .container .button a { 8 | color: #31bc86; 9 | text-decoration: none; 10 | } 11 | 12 | .container .button a:hover, a:focus { 13 | color: #7c8d87; 14 | } 15 | 16 | .container > header { 17 | margin: 0 auto; 18 | text-align: center; 19 | background: rgba(0,0,0,0.01); 20 | } 21 | 22 | .container > header h1 { 23 | font-size: 2.625em; 24 | line-height: 1.3; 25 | margin: 0; 26 | font-weight: 500; 27 | } 28 | 29 | .container > header span { 30 | display: block; 31 | font-size: 45%; 32 | opacity: 0.6; 33 | padding-top: 0.5em 34 | } 35 | 36 | /* Component styles */ 37 | .component { 38 | line-height: 1.5em; 39 | margin: 0 auto; 40 | padding: 0; 41 | width: 100%; 42 | max-width: 1200px; 43 | overflow: hidden; 44 | } 45 | 46 | .component h3 { 47 | font-size: 1.11em; 48 | margin-block-start: 0em; 49 | margin-block-end: 0em; 50 | } 51 | 52 | table { 53 | border: 1px solid black; 54 | border-collapse: collapse; 55 | margin-bottom: 3em; 56 | width: 100%; 57 | background: #fff; 58 | } 59 | 60 | td, th { 61 | border: 1px solid black; 62 | padding: 0.75em 1.5em; 63 | text-align: left; 64 | } 65 | td.err { 66 | background-color: #e992b9; 67 | color: #fff; 68 | font-size: 0.75em; 69 | text-align: center; 70 | line-height: 1; 71 | } 72 | 73 | th { 74 | background-color: #31bc86; 75 | font-weight: bold; 76 | color: #fff; 77 | white-space: nowrap; 78 | } 79 | 80 | tbody th { 81 | background-color: #2ea879; 82 | } 83 | 84 | tbody tr:hover { 85 | background-color: rgba(129,208,177,.3); 86 | } 87 | 88 | /* circle styles */ 89 | .detail { 90 | display: none; 91 | } 92 | .column-percentage:hover .detail { 93 | display: block; 94 | text-align: center; 95 | } 96 | 97 | .single-chart { 98 | width: 100%; 99 | justify-content: space-around ; 100 | } 101 | 102 | .circular-chart { 103 | display: block; 104 | margin: 0px auto; 105 | max-width: 100%; 106 | max-height: 50px; 107 | } 108 | 109 | .circle-bg { 110 | fill: none; 111 | stroke: #eee; 112 | stroke-width: 3.8; 113 | } 114 | 115 | .circle { 116 | fill: none; 117 | stroke-width: 2.8; 118 | stroke-linecap: round; 119 | animation: progress 1s ease-out forwards; 120 | } 121 | 122 | @keyframes progress { 123 | 0% { 124 | stroke-dasharray: 0 100; 125 | } 126 | } 127 | 128 | .circular-chart.red .circle { 129 | stroke: #e71837; 130 | } 131 | 132 | .circular-chart.orange .circle { 133 | stroke: #ff9f00; 134 | } 135 | 136 | .circular-chart.green .circle { 137 | stroke: #4CC790; 138 | } 139 | 140 | .circular-chart.blue .circle { 141 | stroke: #3c9ee5; 142 | } 143 | 144 | .circular-chart.gold .circle { 145 | stroke: #ffd700; 146 | fill: #ffd700; 147 | } 148 | 149 | .percentage { 150 | fill: #666; 151 | font-size: 0.6em; 152 | text-anchor: middle; 153 | } 154 | 155 | /* medal styles */ 156 | .medal { 157 | width: 100px; 158 | height: 60px; 159 | } 160 | 161 | .ribbon { 162 | width: 40px; 163 | height: 35px; 164 | margin: 0 auto; 165 | position: relative; 166 | } 167 | 168 | .ribbon:before, 169 | .ribbon:after { 170 | content: ''; 171 | position: absolute; 172 | width: 17.5px; 173 | height: 100%; 174 | top: 0; 175 | } 176 | 177 | .ribbon:before { 178 | right: 0; 179 | background: #30110E; 180 | transform: skew(-28deg); 181 | } 182 | 183 | .ribbon:after { 184 | background: #EF7E76; 185 | transform: skew(28deg); 186 | } 187 | 188 | .coin { 189 | border: 1px solid #CA5D3E; 190 | border-radius: 50%; 191 | background: #F0CD73; 192 | width: 35px; 193 | height: 35px; 194 | position: relative; 195 | margin: -7.5px auto 0 auto; 196 | box-shadow: 0px 0px 3px 0px #989898; 197 | } 198 | 199 | .coin:after { 200 | content: ''; 201 | position: absolute; 202 | transform: translate(-50%, -50%); 203 | top: 50%; 204 | left: 50%; 205 | width: 17.5px; 206 | height: 17.5px; 207 | border-radius: inherit; 208 | box-shadow: 0 0 0 9px #D9B867; 209 | } 210 | 211 | /* Buttons Style */ 212 | .button { 213 | padding-top: 1em; 214 | font-size: 0.8em; 215 | } 216 | 217 | .button a { 218 | display: inline-block; 219 | margin: 0.5em; 220 | padding: 0.7em 1.1em; 221 | outline: none; 222 | border: 2px solid #31bc86; 223 | text-decoration: none; 224 | text-transform: uppercase; 225 | letter-spacing: 1px; 226 | font-weight: 700; 227 | color: #DB7093; 228 | } 229 | 230 | .button a:hover, 231 | .button a.current-page, 232 | .button a.current-page:hover { 233 | border-color: #DB7093; 234 | color: #DB7093; 235 | } 236 | -------------------------------------------------------------------------------- /azdev/operations/cmdcov/component.js: -------------------------------------------------------------------------------- 1 | var tag=1; 2 | function sortNumberAS(a, b) 3 | { 4 | return a - b 5 | } 6 | function sortNumberDesc(a, b) 7 | { 8 | return b - a 9 | } 10 | function sortStrAS(a, b) 11 | { 12 | x = a.toUpperCase(); 13 | y = b.toUpperCase(); 14 | if (x < y) { 15 | return -1; 16 | } 17 | if (x > y) { 18 | return 1; 19 | } 20 | } 21 | function sortStrDesc(a, b) 22 | { 23 | x = a.toUpperCase(); 24 | y = b.toUpperCase(); 25 | if (x < y) { 26 | return 1; 27 | } 28 | if (x > y) { 29 | return -1; 30 | } 31 | } 32 | function sortTextAS(a, b) 33 | { 34 | if (a === 'Not applicable' || a === 'N/A') { 35 | a = -1; 36 | } else if (a === '') { 37 | a = 100; 38 | } else { 39 | a = parseFloat(a.substr(0, a.length - 1)); 40 | } 41 | if (b === 'Not applicable' || b === 'N/A') { 42 | b = -1; 43 | } else if (b === '') { 44 | b = 100; 45 | } else { 46 | b = parseFloat(b.substr(0, b.length - 1)); 47 | } 48 | if (a < b) { 49 | return -1; 50 | } 51 | if (a > b) { 52 | return 1; 53 | } 54 | return 0; // In case a and b are equal 55 | } 56 | function sortTextDesc(a, b) 57 | { 58 | if (a === 'Not applicable' || a === 'N/A') { 59 | a = -1; 60 | } else if (a === '') { 61 | a = 100; 62 | } else { 63 | a = parseFloat(a.substr(0, a.length - 1)); 64 | } 65 | if (b === 'Not applicable' || b === 'N/A') { 66 | b = -1; 67 | } else if (b === '') { 68 | b = 100; 69 | } else { 70 | b = parseFloat(b.substr(0, b.length - 1)); 71 | } 72 | if (a < b) { 73 | return 1; 74 | } 75 | if (a > b) { 76 | return -1; 77 | } 78 | return 0; // In case a and b are equal 79 | } 80 | 81 | 82 | function SortTable(obj){ 83 | var column=obj.id 84 | var tdModule=document.getElementsByName("td-module"); 85 | var tdTested=document.getElementsByName("td-tested"); 86 | var tdUntested=document.getElementsByName("td-untested"); 87 | var tdPercentage=document.getElementsByName("td-percentage"); 88 | var tdDetail=document.getElementsByClassName("detail") 89 | var tdReport=document.getElementsByName("td-report"); 90 | var tdModuleArray=[]; 91 | var tdTestedArray=[]; 92 | var tdUntestedArray=[]; 93 | var tdPercentageArray=[]; 94 | var tdReportArray=[]; 95 | for(var i=0;i 3 | 4 | 5 | 6 | CLI {{ description }} Test Coverage 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

CLI {{ description }} Test Coverage Report 16 | Please scroll down to see the every module test coverage.
17 | Any question please contact Azure Cli Team.
18 |

19 | {% if enable_cli_own == true -%} 20 | 24 | {% else -%} 25 | 28 | {% endif -%} 29 |
30 |
31 |

Date: {{ date }}

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {{ render_percentage(Total[2], Total[3]) }} 48 | 49 | 50 | {% for module, cov in command_test_coverage.items() %} 51 | 52 | 53 | 54 | 55 | {{ render_percentage(cov[2], cov[3]) }} 56 | 57 | 58 | {% endfor %} 59 | 60 |
ModuleTestedUntestedPercentageReports
Total{{ Total[0] }}{{ Total[1] }}N/A
{{ module }}{{ cov[0] }}{{ cov[1] }}{{ module }} test coverage report
61 |

This is the {{ description.lower() }} test coverage report of CLI.
62 | Any question please contact Azure Cli Team.

63 |
64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /azdev/operations/cmdcov/index2.j2: -------------------------------------------------------------------------------- 1 | {% from "_macros.j2" import render_percentage %} 2 | 3 | 4 | 5 | 6 | CLI Own {{ description }} Test Coverage 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

CLI Own {{ description }} Test Coverage Report 16 | Please scroll down to see the every module test coverage.
17 | Any question please contact Azure Cli Team.
18 |

19 | 23 |
24 |
25 |

Date: {{ date }}

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{ render_percentage(Total[2], Total[3]) }} 42 | 43 | 44 | {% for module, cov in command_test_coverage.items() %} 45 | 46 | 47 | 48 | 49 | {{ render_percentage(cov[2], cov[3]) }} 50 | 51 | 52 | {% endfor %} 53 | 54 |
ModuleTestedUntestedPercentageReports
Total{{ Total[0] }}{{ Total[1] }}N/A
{{ module }}{{ cov[0] }}{{ cov[1] }}{{ module }} test coverage report
55 |

This is the {{ description.lower() }} test coverage report of CLI Own.
56 | Any question please contact Azure Cli Team.

57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /azdev/operations/cmdcov/module.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ module }} Test Coverage Detail 6 | 7 | 8 | 9 | 10 |
11 |
12 |

{{ module }} Test Coverage Report 13 | This is the test coverage report of {{ module }}. Please scroll down to see the untested details.
14 | Any question please contact Azure Cli Team.
15 |

16 | {% if enable_cli_own == true -%} 17 | 21 | {% else -%} 22 | 25 | {% endif -%} 26 |
27 |
28 |

Date: {{ date }}

29 |

Tested: {{ coverage[0] }}, Untested: {{ coverage[1] }}, Percentage: {{ coverage[2] }}

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% for cmd in untested_commands %} 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
ModuleUntested
{{ module }}{{ cmd }}
46 |

This is the test coverage report of {{ module }} module.
47 | Any question please contact Azure Cli Team.
48 |

49 |
50 |
51 | 52 | -------------------------------------------------------------------------------- /azdev/operations/command_change/util.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | import json 7 | import os 8 | import re 9 | from enum import Enum 10 | import jsbeautifier 11 | from knack.log import get_logger 12 | 13 | logger = get_logger(__name__) 14 | 15 | SUBGROUP_NAME_PATTERN = re.compile(r"\[\'sub_groups\'\]\[\'([a-zA-Z0-9\-\s]+)\'\]") 16 | CMD_NAME_PATTERN = re.compile(r"\[\'commands\'\]\[\'([a-zA-Z0-9\-\s]+)\'\]") 17 | CMD_PARAMETER_PROPERTY_PATTERN = re.compile(r"\[(.*?)\]") 18 | 19 | 20 | class ChangeType(int, Enum): 21 | DEFAULT = 0 22 | ADD = 1 23 | CHANGE = 2 24 | REMOVE = 3 25 | 26 | 27 | def get_command_tree(command_name): 28 | """ 29 | input: monitor log-profiles create 30 | ret: 31 | { 32 | is_group: True, 33 | group_name: 'monitor', 34 | sub_info: { 35 | is_group: True, 36 | group_name: 'monitor log-profiles', 37 | sub_info: { 38 | is_group: False, 39 | cmd_name: 'monitor log-profiles create' 40 | } 41 | } 42 | } 43 | """ 44 | name_arr = command_name.split() 45 | ret = {} 46 | name_arr.reverse() 47 | for i, _ in enumerate(name_arr): 48 | tmp = {} 49 | if i == 0: 50 | tmp = { 51 | "is_group": False, 52 | "cmd_name": " ".join(name_arr[::-1]) 53 | } 54 | else: 55 | tmp = { 56 | "is_group": True, 57 | "group_name": " ".join(name_arr[len(name_arr): (i - 1): -1]), 58 | "sub_info": ret 59 | } 60 | ret = tmp 61 | return ret 62 | 63 | 64 | def export_commands_meta(commands_meta, meta_output_path=None): 65 | options = jsbeautifier.default_options() 66 | options.indent_size = 4 67 | for key, module_info in commands_meta.items(): 68 | file_name = "az_" + key + "_meta.json" 69 | if meta_output_path: 70 | file_name = meta_output_path + "/" + file_name 71 | file_folder = os.path.dirname(file_name) 72 | if file_folder and not os.path.exists(file_folder): 73 | os.makedirs(file_folder) 74 | with open(file_name, "w") as f_out: 75 | f_out.write(jsbeautifier.beautify(json.dumps(module_info), options)) 76 | 77 | 78 | def add_to_command_tree(tree, key, value): 79 | parts = key.split() 80 | subtree = tree 81 | for part in parts[:-1]: 82 | if not subtree.get(part): 83 | subtree[part] = {} 84 | subtree = subtree[part] 85 | subtree[parts[-1]] = value 86 | 87 | 88 | def dump_command_tree(command_tree, output_file): 89 | with open(output_file, 'w', encoding='utf-8') as f: 90 | json.dump(command_tree, f, indent=4) 91 | -------------------------------------------------------------------------------- /azdev/operations/extensions/util.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import json 9 | import re 10 | import zipfile 11 | 12 | from knack.util import CLIError 13 | 14 | from azdev.utilities import EXTENSION_PREFIX 15 | 16 | 17 | WHEEL_INFO_RE = re.compile( 18 | r"""^(?P(?P.+?)(-(?P\d.+?))?) 19 | ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) 20 | \.whl|\.dist-info)$""", 21 | re.VERBOSE).match 22 | 23 | 24 | def _get_extension_modname(ext_dir): 25 | # Modification of https://github.com/Azure/azure-cli/blob/dev/src/azure-cli-core/azure/cli/core/extension.py#L153 26 | pos_mods = [n for n in os.listdir(ext_dir) 27 | if n.startswith(EXTENSION_PREFIX) and os.path.isdir(os.path.join(ext_dir, n))] 28 | if len(pos_mods) != 1: 29 | raise AssertionError("Expected 1 module to load starting with " 30 | "'{}': got {}".format(EXTENSION_PREFIX, pos_mods)) 31 | return pos_mods[0] 32 | 33 | 34 | def _get_azext_metadata(ext_dir): 35 | # Modification of https://github.com/Azure/azure-cli/blob/dev/src/azure-cli-core/azure/cli/core/extension.py#L109 36 | AZEXT_METADATA_FILENAME = 'azext_metadata.json' 37 | azext_metadata = None 38 | ext_modname = _get_extension_modname(ext_dir=ext_dir) 39 | azext_metadata_filepath = os.path.join(ext_dir, ext_modname, AZEXT_METADATA_FILENAME) 40 | if os.path.isfile(azext_metadata_filepath): 41 | with open(azext_metadata_filepath) as f: 42 | azext_metadata = json.load(f) 43 | return azext_metadata 44 | 45 | 46 | def get_ext_metadata(ext_dir, ext_file, ext_name): 47 | # Modification of https://github.com/Azure/azure-cli/blob/dev/src/azure-cli-core/azure/cli/core/extension.py#L89 48 | WHL_METADATA_FILENAME = 'metadata.json' 49 | with zipfile.ZipFile(ext_file, 'r') as zip_ref: 50 | zip_ref.extractall(ext_dir) 51 | metadata = {} 52 | dist_info_dirs = [f for f in os.listdir(ext_dir) if f.endswith('.dist-info')] 53 | azext_metadata = _get_azext_metadata(ext_dir) 54 | if azext_metadata: 55 | metadata.update(azext_metadata) 56 | for dist_info_dirname in dist_info_dirs: 57 | parsed_dist_info_dir = WHEEL_INFO_RE(dist_info_dirname) 58 | if parsed_dist_info_dir and parsed_dist_info_dir.groupdict().get('name') == ext_name.replace('-', '_'): 59 | whl_metadata_filepath = os.path.join(ext_dir, dist_info_dirname, WHL_METADATA_FILENAME) 60 | if os.path.isfile(whl_metadata_filepath): 61 | with open(whl_metadata_filepath) as f: 62 | metadata.update(json.load(f)) 63 | return metadata 64 | 65 | 66 | def get_whl_from_url(url, filename, tmp_dir, whl_cache=None): 67 | if not whl_cache: 68 | whl_cache = {} 69 | if url in whl_cache: 70 | return whl_cache[url] 71 | import requests 72 | r = requests.get(url, stream=True, timeout=10) 73 | try: 74 | assert r.status_code == 200, "Request to {} failed with {}".format(url, r.status_code) 75 | except AssertionError: 76 | raise CLIError("unable to download (status code {}): {}".format(r.status_code, url)) 77 | ext_file = os.path.join(tmp_dir, filename) 78 | with open(ext_file, 'wb') as f: 79 | for chunk in r.iter_content(chunk_size=1024): 80 | if chunk: # ignore keep-alive new chunks 81 | f.write(chunk) 82 | whl_cache[url] = ext_file 83 | return ext_file 84 | 85 | 86 | def get_pkg_info_from_pkg_metafile(pkg_info_file): 87 | mod_info = {} 88 | with open(pkg_info_file, "r", encoding="utf-8") as f: 89 | for line in f: 90 | if line.startswith("Name:"): 91 | pkg_name = line.split(":")[-1].strip() 92 | mod_info["pkg_name"] = pkg_name 93 | if line.startswith("Version:"): 94 | pkg_version = line.split(":")[-1].strip() 95 | mod_info["pkg_version"] = pkg_version 96 | return mod_info 97 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/cli_docs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-cli-dev-tools/5130a24020ca0a47055e914a58a4af67d9eb98ad/azdev/operations/help/refdoc/cli_docs/__init__.py -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/cli_docs/helpgen.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | import os 7 | import json 8 | 9 | from azure.cli.core._help import CliCommandHelpFile # pylint: disable=import-error 10 | from azure.cli.core.file_util import create_invoker_and_load_cmds_and_args, get_all_help # pylint: disable=import-error 11 | 12 | from azdev.utilities import get_cli_repo_path 13 | from azdev.operations.help import DOC_SOURCE_MAP_PATH 14 | from azdev.operations.help.refdoc.common.directives import AbstractHelpGenDirective 15 | from azdev.operations.help.refdoc.common.directives import setup_common_directives 16 | 17 | 18 | class HelpGenDirective(AbstractHelpGenDirective): 19 | """ General CLI Sphinx Directive 20 | The Core CLI has a doc source map to determine help text source for core cli commands. Extension help processed 21 | here will have no doc source 22 | """ 23 | 24 | def _get_help_files(self, az_cli): 25 | create_invoker_and_load_cmds_and_args(az_cli) 26 | return get_all_help(az_cli) 27 | 28 | def _load_doc_source_map(self): 29 | map_path = os.path.join(get_cli_repo_path(), DOC_SOURCE_MAP_PATH) 30 | with open(map_path) as open_file: 31 | return json.load(open_file) 32 | 33 | def _get_doc_source_content(self, doc_source_map, help_file): 34 | is_command = isinstance(help_file, CliCommandHelpFile) 35 | result = None 36 | if not is_command: 37 | top_group_name = help_file.command.split()[0] if help_file.command else 'az' 38 | doc_source_value = doc_source_map[top_group_name] if top_group_name in doc_source_map else '' 39 | result = '{}:docsource: {}'.format(self._INDENT, doc_source_value) 40 | else: 41 | top_command_name = help_file.command.split()[0] if help_file.command else '' 42 | if top_command_name in doc_source_map: 43 | result = '{}:docsource: {}'.format(self._INDENT, doc_source_map[top_command_name]) 44 | return result 45 | 46 | 47 | def setup(app): 48 | """ Setup sphinx app with help generation directive. This is called by sphinx. 49 | :param app: The sphinx app 50 | :return: 51 | """ 52 | app.add_directive('corehelpgen', HelpGenDirective) 53 | setup_common_directives(app) 54 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/cli_docs/ind.rst: -------------------------------------------------------------------------------- 1 | .. corehelpgen:: 2 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/common/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See License.txt in the project root for license information. 5 | # -------------------------------------------------------------------------------------------- 6 | # -*- coding: utf-8 -*- 7 | 8 | # -- General configuration ------------------------------------------------ 9 | # For more information on all config options, see http://www.sphinx-doc.org/en/stable/config.html 10 | 11 | # Add any Sphinx extension module names here, as strings. They can be 12 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 13 | # ones. 14 | 15 | extensions = [ 16 | 'azdev.operations.help.refdoc.cli_docs.helpgen', 17 | 'azdev.operations.help.refdoc.extension_docs.helpgen' 18 | ] 19 | 20 | # The file name extension for the sphinx source files. 21 | source_suffix = '.rst' 22 | 23 | # The master toctree document. 24 | master_doc = 'ind' 25 | 26 | # General information about the project. 27 | project = 'az' 28 | copyright = '2019, msft' # pylint: disable=redefined-builtin 29 | author = 'msft' 30 | 31 | # The language for content autogenerated by Sphinx 32 | language = None 33 | 34 | # List of patterns, relative to source directory, that match files and 35 | # directories to ignore when looking for source files. 36 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 37 | 38 | pygments_style = 'sphinx' 39 | 40 | # Smart quotes is true by default, however the previous doc gen in the extensions repo sets it to false. 41 | # So, the extension doc-generation command overrides this and sets smartquotes to false via sphinx-build's `-D` option 42 | # Doing this makes it to compare the behavior of azdev to existing doc gen scripts. This setting is not necessary. 43 | smartquotes = True 44 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/extension_docs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-cli-dev-tools/5130a24020ca0a47055e914a58a4af67d9eb98ad/azdev/operations/help/refdoc/extension_docs/__init__.py -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/extension_docs/helpgen.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | from knack.log import get_logger 7 | from knack.help import GroupHelpFile 8 | 9 | from azure.cli.core.file_util import _store_parsers, _is_group # pylint: disable=import-error 10 | from azure.cli.core.commands import ExtensionCommandSource # pylint: disable=import-error 11 | from azure.cli.core._help import CliCommandHelpFile # pylint: disable=import-error 12 | 13 | from azdev.operations.help.refdoc.common.directives import AbstractHelpGenDirective 14 | from azdev.operations.help.refdoc.common.directives import setup_common_directives 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class ExtensionHelpGenDirective(AbstractHelpGenDirective): 20 | """ 21 | CLI Extensions Sphinx Directive 22 | Ensure that sphinx output is generated from only extension help files 23 | """ 24 | 25 | def _get_help_files(self, az_cli): 26 | return get_extension_help_files(az_cli) 27 | 28 | def _load_doc_source_map(self): 29 | # no doc source map for extensions 30 | pass 31 | 32 | def _get_doc_source_content(self, doc_source_map, help_file): 33 | # no doc source map for extensions 34 | pass 35 | 36 | 37 | # TODO: move this into core 38 | def get_extension_help_files(cli_ctx): 39 | 40 | # 1. Create invoker and load command table and arguments. Remember to turn off applicability check. 41 | invoker = cli_ctx.invocation_cls(cli_ctx=cli_ctx, commands_loader_cls=cli_ctx.commands_loader_cls, 42 | parser_cls=cli_ctx.parser_cls, help_cls=cli_ctx.help_cls) 43 | cli_ctx.invocation = invoker 44 | 45 | invoker.commands_loader.skip_applicability = True 46 | cmd_table = invoker.commands_loader.load_command_table(None) 47 | 48 | # turn off applicability check for all loaders 49 | for loaders in invoker.commands_loader.cmd_to_loader_map.values(): 50 | for loader in loaders: 51 | loader.skip_applicability = True 52 | 53 | # filter the command table to only get commands from extensions 54 | cmd_table = {k: v for k, v in cmd_table.items() if isinstance(v.command_source, ExtensionCommandSource)} 55 | invoker.commands_loader.command_table = cmd_table 56 | logger.warning('Found %s command(s) from the extension.\n', len(cmd_table)) 57 | 58 | for cmd_name in cmd_table: 59 | invoker.commands_loader.load_arguments(cmd_name) 60 | 61 | invoker.parser.load_command_table(invoker.commands_loader) 62 | 63 | # 2. Now load applicable help files 64 | parser_keys = [] 65 | parser_values = [] 66 | sub_parser_keys = [] 67 | sub_parser_values = [] 68 | _store_parsers(invoker.parser, parser_keys, parser_values, sub_parser_keys, sub_parser_values) 69 | for cmd, parser in zip(parser_keys, parser_values): 70 | if cmd not in sub_parser_keys: 71 | sub_parser_keys.append(cmd) 72 | sub_parser_values.append(parser) 73 | help_ctx = cli_ctx.help_cls(cli_ctx=cli_ctx) 74 | help_files = [] 75 | for cmd, parser in zip(sub_parser_keys, sub_parser_values): 76 | try: 77 | help_file = GroupHelpFile(help_ctx, cmd, parser) if _is_group(parser) \ 78 | else CliCommandHelpFile(help_ctx, cmd, parser) 79 | help_file.load(parser) 80 | help_files.append(help_file) 81 | except Exception as ex: # pylint: disable=broad-except 82 | logger.warning("Skipped '%s' due to '%s'", cmd, ex) 83 | help_files = sorted(help_files, key=lambda x: x.command) 84 | logger.warning('Generated %s help objects from the extension.\n', len(help_files)) 85 | return help_files 86 | 87 | 88 | def setup(app): 89 | """ Setup sphinx app with help generation directive. This is called by sphinx. 90 | :param app: The sphinx app 91 | :return: 92 | """ 93 | app.add_directive('exthelpgen', ExtensionHelpGenDirective) 94 | setup_common_directives(app) 95 | -------------------------------------------------------------------------------- /azdev/operations/help/refdoc/extension_docs/ind.rst: -------------------------------------------------------------------------------- 1 | .. exthelpgen:: 2 | -------------------------------------------------------------------------------- /azdev/operations/legal.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import pathlib 9 | 10 | from knack.util import CLIError 11 | 12 | from azdev.utilities import ( 13 | display, heading, subheading, get_cli_repo_path, get_ext_repo_paths) 14 | 15 | 16 | LICENSE_HEADER = """# Copyright (c) Microsoft Corporation. All rights reserved. 17 | # Licensed under the MIT License. See License.txt in the project root for license information. 18 | """ 19 | 20 | WRAPPED_LICENSE_HEADER = """# Copyright (c) Microsoft Corporation. All rights reserved. 21 | # Licensed under the MIT License. See License.txt in the project root for 22 | # license information. 23 | """ 24 | 25 | 26 | _IGNORE_SUBDIRS = ['__pycache__', 'vendored_sdks', 'site-packages', 'env'] 27 | 28 | 29 | def check_license_headers(): 30 | 31 | heading('Verify License Headers') 32 | 33 | cli_path = get_cli_repo_path() 34 | all_paths = [cli_path] 35 | for path in get_ext_repo_paths(): 36 | all_paths.append(path) 37 | 38 | files_without_header = [] 39 | for path in all_paths: 40 | py_files = pathlib.Path(path).glob('**' + os.path.sep + '*.py') 41 | 42 | for py_file in py_files: 43 | py_file = str(py_file) 44 | 45 | if py_file.endswith('azure_cli_bdist_wheel.py'): 46 | continue 47 | 48 | for ignore_token in _IGNORE_SUBDIRS: 49 | if ignore_token in py_file: 50 | break 51 | else: 52 | with open(str(py_file), 'r', encoding='utf-8') as f: 53 | file_text = f.read() 54 | 55 | if not file_text: 56 | continue 57 | 58 | test_results = [ 59 | LICENSE_HEADER in file_text, 60 | WRAPPED_LICENSE_HEADER in file_text 61 | ] 62 | if not any(test_results): 63 | files_without_header.append(py_file) 64 | 65 | subheading('Results') 66 | if files_without_header: 67 | raise CLIError("{}\nError: {} files don't have the required license headers.".format( 68 | '\n'.join(files_without_header), len(files_without_header))) 69 | display('License headers verified OK.') 70 | -------------------------------------------------------------------------------- /azdev/operations/linter/data/cmd_example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "create": 1, 3 | "add": 1, 4 | "update": 1, 5 | "list": 0, 6 | "delete": 0, 7 | "remove": 0, 8 | "show": 0, 9 | "wait": 0 10 | } -------------------------------------------------------------------------------- /azdev/operations/linter/pylint_checkers/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/operations/linter/pylint_checkers/show_command.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | import astroid 7 | 8 | from pylint.checkers import BaseChecker 9 | 10 | 11 | class ShowCommandChecker(BaseChecker): 12 | 13 | name = 'show-command' 14 | priority = -1 15 | msgs = { 16 | 'E5001': ( 17 | 'Show command must use show_command or custom_show_command.', 18 | 'show-command', 19 | 'Please use show_command or custom_show_command.' 20 | ), 21 | } 22 | 23 | def visit_call(self, node): 24 | try: 25 | if not (isinstance(node.args[0], astroid.node_classes.Const) and node.args[0].value == 'show'): 26 | return 27 | if node.func.attrname in ('command', 'custom_command'): 28 | self.add_message( 29 | 'show-command', node=node, 30 | ) 31 | except IndexError: 32 | return 33 | 34 | 35 | def register(linter): 36 | linter.register_checker(ShowCommandChecker(linter)) 37 | -------------------------------------------------------------------------------- /azdev/operations/linter/rule_decorators.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | from knack.util import CLIError 8 | from .linter import RuleError, LinterSeverity 9 | 10 | 11 | class BaseRule: 12 | 13 | def __init__(self, severity): 14 | if severity not in LinterSeverity: 15 | raise CLIError("A {} rule has an invalid severity. Received {}; expected one of: {}" 16 | .format(str(self.__class__), severity, list(LinterSeverity))) 17 | self.severity = severity 18 | 19 | 20 | # command_test_rule run once 21 | class CommandCoverageRule(BaseRule): 22 | 23 | def __call__(self, func): 24 | def add_to_linter(linter_manager): 25 | def wrapper(): 26 | linter = linter_manager.linter 27 | try: 28 | func(linter) 29 | except RuleError as ex: 30 | linter_manager.mark_rule_failure(self.severity) 31 | yield (_create_violation_msg(ex, 'Repo: {}, Src Branch: {}, Target Branch: {}', 32 | linter.git_repo, linter.git_source, linter.git_target), 33 | (linter.git_source, linter.git_target), 34 | func.__name__) 35 | 36 | linter_manager.add_rule('command_test_coverage', func.__name__, wrapper, self.severity) 37 | 38 | add_to_linter.linter_rule = True 39 | return add_to_linter 40 | 41 | 42 | # help_file_entry_rule 43 | class HelpFileEntryRule(BaseRule): 44 | 45 | def __call__(self, func): 46 | return _get_decorator(func, 'help_file_entries', 'Help-Entry: `{}`', self.severity) 47 | 48 | 49 | # command_rule 50 | class CommandRule(BaseRule): 51 | 52 | def __call__(self, func): 53 | return _get_decorator(func, 'commands', 'Command: `{}`', self.severity) 54 | 55 | 56 | # command_group_rule 57 | class CommandGroupRule(BaseRule): 58 | 59 | def __call__(self, func): 60 | return _get_decorator(func, 'command_groups', 'Command-Group: `{}`', self.severity) 61 | 62 | 63 | # parameter_rule 64 | class ParameterRule(BaseRule): 65 | 66 | def __call__(self, func): 67 | def add_to_linter(linter_manager): 68 | def wrapper(): 69 | linter = linter_manager.linter 70 | 71 | for command_name in linter.commands: 72 | for parameter_name in linter.get_command_parameters(command_name): 73 | exclusion_parameters = linter_manager.exclusions.get(command_name, {}).get('parameters', {}) 74 | exclusions = exclusion_parameters.get(parameter_name, {}).get('rule_exclusions', []) 75 | if func.__name__ not in exclusions: 76 | try: 77 | func(linter, command_name, parameter_name) 78 | except RuleError as ex: 79 | linter_manager.mark_rule_failure(self.severity) 80 | yield (_create_violation_msg(ex, 'Parameter: {}, `{}`', command_name, parameter_name), 81 | (command_name, parameter_name), 82 | func.__name__) 83 | 84 | linter_manager.add_rule('params', func.__name__, wrapper, self.severity) 85 | add_to_linter.linter_rule = True 86 | return add_to_linter 87 | 88 | 89 | def _get_decorator(func, rule_group, print_format, severity): 90 | def add_to_linter(linter_manager): 91 | def wrapper(): 92 | linter = linter_manager.linter 93 | # print('enter add to linter', len(getattr(linter, rule_group))) 94 | for iter_entity in getattr(linter, rule_group): 95 | exclusions = linter_manager.exclusions.get(iter_entity, {}).get('rule_exclusions', []) 96 | if func.__name__ not in exclusions: 97 | try: 98 | func(linter, iter_entity) 99 | except RuleError as ex: 100 | linter_manager.mark_rule_failure(severity) 101 | yield (_create_violation_msg(ex, print_format, iter_entity), iter_entity, func.__name__) 102 | 103 | linter_manager.add_rule(rule_group, func.__name__, wrapper, severity) 104 | add_to_linter.linter_rule = True 105 | return add_to_linter 106 | 107 | 108 | def _create_violation_msg(ex, format_string, *format_args): 109 | violation_string = format_string.format(*format_args) 110 | return ' {} - {}'.format(violation_string, ex) 111 | -------------------------------------------------------------------------------- /azdev/operations/linter/rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-cli-dev-tools/5130a24020ca0a47055e914a58a4af67d9eb98ad/azdev/operations/linter/rules/__init__.py -------------------------------------------------------------------------------- /azdev/operations/linter/rules/ci_exclusions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # the following are the rules and the modules that are excluded from those rules in the ci 3 | # note that this is a TEMPORARY stop-gap and the rule violations should be addressed 4 | 5 | no_ids_for_list_commands: 6 | - dla 7 | - dls 8 | - iot 9 | - rdbms 10 | - sql 11 | faulty_help_example_parameters_rule: 12 | - find 13 | 14 | ... # end of document -------------------------------------------------------------------------------- /azdev/operations/linter/rules/command_coverage_rules.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | from ..rule_decorators import CommandCoverageRule 8 | from ..linter import RuleError, LinterSeverity 9 | 10 | 11 | @CommandCoverageRule(LinterSeverity.MEDIUM) 12 | def missing_command_test_coverage(linter): 13 | exec_state, violations = linter.get_command_test_coverage() 14 | if not exec_state: 15 | violation_msg = "\n\t".join(violations) 16 | raise RuleError(violation_msg + "\n") 17 | 18 | 19 | @CommandCoverageRule(LinterSeverity.MEDIUM) 20 | def missing_parameter_test_coverage(linter): 21 | exec_state, violations = linter.get_parameter_test_coverage() 22 | if not exec_state: 23 | violation_msg = "\n\t".join(violations) 24 | raise RuleError(violation_msg + "\n") 25 | 26 | 27 | @CommandCoverageRule(LinterSeverity.HIGH) 28 | def missing_command_example(linter): 29 | violations = linter.check_missing_command_example() 30 | if violations: 31 | violation_msg = "\n\t".join(violations) 32 | raise RuleError(violation_msg + "\n") 33 | -------------------------------------------------------------------------------- /azdev/operations/linter/rules/command_group_rules.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | # pylint: disable=duplicate-code 7 | 8 | from azdev.operations.constant import DISALLOWED_HTML_TAG_RULE_LINK 9 | from ..rule_decorators import CommandGroupRule 10 | from ..linter import RuleError, LinterSeverity 11 | from ..util import has_illegal_html_tag, has_broken_site_links 12 | 13 | 14 | @CommandGroupRule(LinterSeverity.HIGH) 15 | def missing_group_help(linter, command_group_name): 16 | if not linter.get_command_group_help(command_group_name) and not linter.command_group_expired(command_group_name) \ 17 | and command_group_name != '': 18 | raise RuleError('Missing help') 19 | 20 | 21 | @CommandGroupRule(LinterSeverity.HIGH) 22 | def expired_command_group(linter, command_group_name): 23 | if linter.command_group_expired(command_group_name): 24 | raise RuleError("Deprecated command group is expired and should be removed.") 25 | 26 | 27 | @CommandGroupRule(LinterSeverity.MEDIUM) 28 | def require_wait_command_if_no_wait(linter, command_group_name): 29 | # If any command within a command group or subgroup exposes the --no-wait parameter, 30 | # the wait command should be exposed. 31 | 32 | # find commands under this group. A command in this group has one more token than the group name. 33 | group_command_names = [cmd for cmd in linter.commands if cmd.startswith(command_group_name) and 34 | len(cmd.split()) == len(command_group_name.split()) + 1] 35 | 36 | # if one of the commands in this group ends with wait we are good 37 | for cmd in group_command_names: 38 | cmds = cmd.split() 39 | if cmds[-1].lower() == "wait": 40 | return 41 | 42 | # otherwise there is no wait command. If a command in this group has --no-wait, then error out. 43 | for cmd in group_command_names: 44 | if linter.get_command_metadata(cmd).supports_no_wait: 45 | raise RuleError("Group does not have a 'wait' command, yet '{}' exposes '--no-wait'".format(cmd)) 46 | 47 | 48 | @CommandGroupRule(LinterSeverity.HIGH) 49 | def disallowed_html_tag_from_command_group(linter, command_group_name): 50 | if command_group_name == '' or not linter.get_loaded_help_entry(command_group_name): 51 | return 52 | help_entry = linter.get_loaded_help_entry(command_group_name) 53 | if help_entry.short_summary and (disallowed_tags := has_illegal_html_tag(help_entry.short_summary, 54 | linter.diffed_lines)): 55 | raise RuleError("Disallowed html tags {} in short summary. " 56 | "If the content is a placeholder, please remove <> or wrap it with backtick. " 57 | "For example: 1) -res.yaml -> `-res.yaml`; " 58 | "2) http://: -> `http://:`. " 59 | "For more info please refer to: {}".format(disallowed_tags, 60 | DISALLOWED_HTML_TAG_RULE_LINK)) 61 | if help_entry.long_summary and (disallowed_tags := has_illegal_html_tag(help_entry.long_summary, 62 | linter.diffed_lines)): 63 | raise RuleError("Disallowed html tags {} in long summary. " 64 | "If content is a placeholder, please remove <> or wrap it with backtick. " 65 | "For example: 1) -res.yaml -> `-res.yaml`; " 66 | "2) http://: -> `http://:`. " 67 | "For more info please refer to: {}".format(disallowed_tags, 68 | DISALLOWED_HTML_TAG_RULE_LINK)) 69 | 70 | 71 | @CommandGroupRule(LinterSeverity.MEDIUM) 72 | def broken_site_link_from_command_group(linter, command_group_name): 73 | if command_group_name == '' or not linter.get_loaded_help_entry(command_group_name): 74 | return 75 | help_entry = linter.get_loaded_help_entry(command_group_name) 76 | if help_entry.short_summary and (broken_links := has_broken_site_links(help_entry.short_summary, 77 | linter.diffed_lines)): 78 | raise RuleError("Broken links {} in short summary. " 79 | "If link is an example, please wrap it with backtick. ".format(broken_links)) 80 | if help_entry.long_summary and (broken_links := has_broken_site_links(help_entry.long_summary, 81 | linter.diffed_lines)): 82 | raise RuleError("Broken links {} in long summary. " 83 | "If link is an example, please wrap it with backtick. ".format(broken_links)) 84 | -------------------------------------------------------------------------------- /azdev/operations/linter/rules/command_rules.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | # pylint: disable=duplicate-code 7 | 8 | from azdev.operations.constant import DISALLOWED_HTML_TAG_RULE_LINK 9 | from ..rule_decorators import CommandRule 10 | from ..linter import RuleError, LinterSeverity 11 | from ..util import has_illegal_html_tag, has_broken_site_links 12 | 13 | 14 | @CommandRule(LinterSeverity.HIGH) 15 | def missing_command_help(linter, command_name): 16 | if not linter.get_command_help(command_name) and not linter.command_expired(command_name): 17 | raise RuleError('Missing help') 18 | 19 | 20 | @CommandRule(LinterSeverity.HIGH) 21 | def no_ids_for_list_commands(linter, command_name): 22 | if command_name.split()[-1] == 'list' and 'ids' in linter.get_command_parameters(command_name): 23 | raise RuleError('List commands should not expose --ids argument') 24 | 25 | 26 | @CommandRule(LinterSeverity.HIGH) 27 | def expired_command(linter, command_name): 28 | if linter.command_expired(command_name): 29 | raise RuleError('Deprecated command is expired and should be removed.') 30 | 31 | 32 | @CommandRule(LinterSeverity.LOW) 33 | def group_delete_commands_should_confirm(linter, command_name): 34 | # We cannot detect from cmd table etc whether a delete command deletes a collection, group or set of resources. 35 | # so warn users for every delete command. 36 | 37 | if command_name.split()[-1].lower() == "delete": 38 | if 'yes' not in linter.get_command_parameters(command_name): 39 | raise RuleError("If this command deletes a collection, or group of resources. " 40 | "Please make sure to ask for confirmation.") 41 | 42 | 43 | @CommandRule(LinterSeverity.HIGH) 44 | def disallowed_html_tag_from_command(linter, command_name): 45 | if command_name == '' or not linter.get_loaded_help_entry(command_name): 46 | return 47 | help_entry = linter.get_loaded_help_entry(command_name) 48 | if help_entry.short_summary and (disallowed_tags := has_illegal_html_tag(help_entry.short_summary, 49 | linter.diffed_lines)): 50 | raise RuleError("Disallowed html tags {} in short summary. " 51 | "If the content is a placeholder, please remove <> or wrap it with backtick. " 52 | "For example: 1) -res.yaml -> `-res.yaml`; " 53 | "2) http://: -> `http://:`. " 54 | "For more info please refer to: {}".format(disallowed_tags, 55 | DISALLOWED_HTML_TAG_RULE_LINK)) 56 | if help_entry.long_summary and (disallowed_tags := has_illegal_html_tag(help_entry.long_summary, 57 | linter.diffed_lines)): 58 | raise RuleError("Disallowed html tags {} in long summary. " 59 | "If content is a placeholder, please remove <> or wrap it with backtick. " 60 | "For example: 1) -res.yaml -> `-res.yaml`; " 61 | "2) http://: -> `http://:`. " 62 | "For more info please refer to: {}".format(disallowed_tags, 63 | DISALLOWED_HTML_TAG_RULE_LINK)) 64 | 65 | 66 | @CommandRule(LinterSeverity.MEDIUM) 67 | def broken_site_link_from_command(linter, command_name): 68 | if command_name == '' or not linter.get_loaded_help_entry(command_name): 69 | return 70 | help_entry = linter.get_loaded_help_entry(command_name) 71 | if help_entry.short_summary and (broken_links := has_broken_site_links(help_entry.short_summary, 72 | linter.diffed_lines)): 73 | raise RuleError("Broken links {} in short summary. " 74 | "If link is an example, please wrap it with backtick. ".format(broken_links)) 75 | if help_entry.long_summary and (broken_links := has_broken_site_links(help_entry.long_summary, 76 | linter.diffed_lines)): 77 | raise RuleError("Broken links {} in long summary. " 78 | "If link is an example, please wrap it with backtick. ".format(broken_links)) 79 | -------------------------------------------------------------------------------- /azdev/operations/linter/rules/linter_exclusions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # this is an example linter exclusion file 3 | 4 | help-entry-name: 5 | rule_exclusions: 6 | - rule_name1 7 | - rule_name2 8 | command-group-name: 9 | rule_exclusions: 10 | - rule_name1 11 | - rule_name2 12 | # command with only command rule exclusions 13 | command-name1: 14 | rule_exclusions: 15 | - rule_name1 16 | - rule_name2 17 | # command with only parameter rule exclusions 18 | command-name2: 19 | parameters: 20 | parameter_name1: 21 | rule_exclusions: 22 | - rule_name1 23 | - rule_name2 24 | parameter_name2: 25 | rule_exclusions: 26 | - rule_name1 27 | - rule_name2 28 | # command with both command and parameter exclusions 29 | command-name3: 30 | rule_exclusions: 31 | - rule_name1 32 | - rule_name2 33 | parameters: 34 | parameter_name1: 35 | rule_exclusions: 36 | - rule_name1 37 | - rule_name2 38 | parameter_name2: 39 | rule_exclusions: 40 | - rule_name1 41 | - rule_name2 42 | ... -------------------------------------------------------------------------------- /azdev/operations/python_sdk.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | from azdev.utilities import ( 9 | pip_cmd) 10 | 11 | 12 | def install_draft_sdk(modules, private=False): 13 | for module in modules: 14 | kwargs = { 15 | 'module': module, 16 | 'pr': 'pr' if private else '', 17 | 'branch': 'restapi_auto_{}/resource-manager'.format(module) 18 | } 19 | pip_cmd('install "git+https://github.com/Azure/azure-sdk-for-python{pr}@{branch}' 20 | '#egg=azure-mgmt-{module}&subdirectory=azure-mgmt-{module}"'.format(**kwargs), 21 | show_stderr=True, 22 | message='Installing draft SDK for azure-mgmt-{}...'.format(module)) 23 | -------------------------------------------------------------------------------- /azdev/operations/resource.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import json 8 | 9 | from knack.log import get_logger 10 | from knack.prompting import prompt_y_n 11 | from knack.util import CLIError 12 | 13 | from azdev.utilities import ( 14 | cmd as run_cmd, subheading, display, require_azure_cli) 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class Data: 20 | def __init__(self, **kw): 21 | self.__dict__.update(kw) 22 | if 'properties' in self.__dict__: 23 | self.__dict__.update(self.properties) # pylint: disable=no-member 24 | del self.properties # pylint: disable=no-member 25 | 26 | 27 | def delete_groups(prefixes=None, older_than=6, product='azurecli', cause='automation', yes=False): 28 | from datetime import datetime, timedelta 29 | 30 | require_azure_cli() 31 | 32 | groups = json.loads(run_cmd('az group list -ojson').result) 33 | groups_to_delete = [] 34 | 35 | def _filter_by_tags(): 36 | for group in groups: 37 | group = Data(**group) 38 | 39 | if not group.tags: # pylint: disable=no-member 40 | continue 41 | 42 | tags = Data(**group.tags) # pylint: disable=no-member 43 | try: 44 | date_tag = datetime.strptime(tags.date, '%Y-%m-%dT%H:%M:%SZ') 45 | curr_time = datetime.utcnow() 46 | if (tags.product == product and tags.cause == cause and 47 | (curr_time - date_tag <= timedelta(hours=older_than + 1))): 48 | groups_to_delete.append(group.name) 49 | except AttributeError: 50 | continue 51 | 52 | def _filter_by_prefix(): 53 | for group in groups: 54 | group = Data(**group) 55 | 56 | for prefix in prefixes: 57 | if group.name.startswith(prefix): 58 | groups_to_delete.append(group.name) 59 | 60 | def _delete(): 61 | for group in groups_to_delete: 62 | run_cmd('az group delete -g {} -y --no-wait'.format(group), message=True) 63 | 64 | if prefixes: 65 | logger.info('Filter by prefix') 66 | _filter_by_prefix() 67 | else: 68 | logger.info('Filter by tags') 69 | _filter_by_tags() 70 | 71 | if not groups_to_delete: 72 | raise CLIError('No groups meet the criteria to delete.') 73 | 74 | if yes: 75 | _delete() 76 | else: 77 | subheading('Groups to Delete') 78 | for group in groups_to_delete: 79 | display('\t{}'.format(group)) 80 | 81 | if prompt_y_n('Delete {} resource groups?'.format(len(groups_to_delete)), 'y'): 82 | _delete() 83 | else: 84 | raise CLIError('Command cancelled.') 85 | -------------------------------------------------------------------------------- /azdev/operations/statistics/util.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for license information. 4 | # -------------------------------------------------------------------------------------------- 5 | 6 | import copy 7 | 8 | from knack.log import get_logger 9 | 10 | from azdev.utilities import get_name_index 11 | 12 | 13 | logger = get_logger(__name__) 14 | 15 | 16 | def filter_modules(command_loader, modules=None, exclude=False, include_whl_extensions=False): 17 | modules = modules or [] 18 | 19 | # command tables and help entries must be copied to allow for seperate linter scope 20 | command_table = command_loader.command_table.copy() 21 | command_group_table = command_loader.command_group_table.copy() 22 | command_loader = copy.copy(command_loader) 23 | command_loader.command_table = command_table 24 | command_loader.command_group_table = command_group_table 25 | name_index = get_name_index(include_whl_extensions=include_whl_extensions) 26 | 27 | for command_name in list(command_loader.command_table.keys()): 28 | try: 29 | source_name, _ = _get_command_source(command_name, command_loader.command_table) 30 | except ValueError as ex: 31 | # command is unrecognized 32 | logger.warning(ex) 33 | source_name = None 34 | 35 | try: 36 | long_name = name_index[source_name] 37 | is_specified = source_name in modules or long_name in modules 38 | except KeyError: 39 | is_specified = False 40 | if is_specified == exclude: 41 | # brute force method of ignoring commands from a module or extension 42 | command_loader.command_table.pop(command_name, None) 43 | 44 | # Remove unneeded command groups 45 | retained_command_groups = {' '.join(x.split(' ')[:-1]) for x in command_loader.command_table} 46 | excluded_command_groups = set(command_loader.command_group_table.keys()) - retained_command_groups 47 | 48 | for group_name in excluded_command_groups: 49 | command_loader.command_group_table.pop(group_name, None) 50 | 51 | return command_loader 52 | 53 | 54 | def _get_command_source(command_name, command_table): 55 | from azure.cli.core.commands import ExtensionCommandSource # pylint: disable=import-error 56 | command = command_table.get(command_name) 57 | # see if command is from an extension 58 | if isinstance(command.command_source, ExtensionCommandSource): 59 | return command.command_source.extension_name, True 60 | if command.command_source is None: 61 | raise ValueError('Command: `%s`, has no command source.' % command_name) 62 | # command is from module 63 | return command.command_source, False 64 | -------------------------------------------------------------------------------- /azdev/operations/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/operations/tests/files/email_string.txt: -------------------------------------------------------------------------------- 1 | This is a test string with email fooabc@gmail.com. -------------------------------------------------------------------------------- /azdev/operations/tests/files/simple_string.txt: -------------------------------------------------------------------------------- 1 | This is a test string without any secrets. -------------------------------------------------------------------------------- /azdev/operations/tests/files/subdir/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", 3 | "sas": "sv=2022-11-02&sr=c&sig=a9Y5mpQgKUiiPzHFNdDm53Na6UndTrNMCsRZd6b2oV4%3D", 4 | "detail": { 5 | "email": "fooabc@gmail.com" 6 | } 7 | } -------------------------------------------------------------------------------- /azdev/operations/tests/jsons/az_ams_meta_after.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "ams", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "ams": { 7 | "name": "ams", 8 | "commands": {}, 9 | "sub_groups": { 10 | "ams asset": { 11 | "name": "ams asset", 12 | "commands": { 13 | "ams asset get-sas-urls": { 14 | "name": "ams asset get-sas-urls", 15 | "is_aaz": false, 16 | "parameters": [{ 17 | "name": "resource_group_name", 18 | "options": ["--resource-group", "-g"], 19 | "required": true, 20 | "id_part": "resource_group" 21 | }, { 22 | "name": "account_name", 23 | "options": ["--account-name", "-a"], 24 | "required": true, 25 | "id_part": "name" 26 | }, { 27 | "name": "asset_name", 28 | "options": ["--name", "-n"], 29 | "required": true, 30 | "id_part": "child_name_1" 31 | }, { 32 | "name": "permissions", 33 | "options": ["--permissions"], 34 | "choices": ["Read", "ReadWrite", "ReadWriteDelete"], 35 | "default": "Read" 36 | }, { 37 | "name": "expiry_time", 38 | "options": ["--expiry"], 39 | "type": "custom_type", 40 | "default": "2023-07-10 18:47:05.586776" 41 | }] 42 | } 43 | }, 44 | "sub_groups": {} 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /azdev/operations/tests/jsons/az_ams_meta_before.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "ams", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "ams": { 7 | "name": "ams", 8 | "commands": {}, 9 | "sub_groups": { 10 | "ams asset": { 11 | "name": "ams asset", 12 | "commands": { 13 | "ams asset get-sas-urls": { 14 | "name": "ams asset get-sas-urls", 15 | "is_aaz": false, 16 | "parameters": [{ 17 | "name": "resource_group_name", 18 | "options": ["--resource-group", "-g"], 19 | "required": true, 20 | "id_part": "resource_group" 21 | }, { 22 | "name": "account_name", 23 | "options": ["--account-name", "-a"], 24 | "required": true, 25 | "id_part": "name" 26 | }, { 27 | "name": "asset_name", 28 | "options": ["--name", "-n"], 29 | "required": true, 30 | "id_part": "child_name_1" 31 | }, { 32 | "name": "permissions", 33 | "options": ["--permissions"], 34 | "choices": ["Read", "ReadWrite", "ReadWriteDelete"], 35 | "default": "Read" 36 | }, { 37 | "name": "expiry_time", 38 | "options": ["--expiry"], 39 | "type": "custom_type", 40 | "default": "2023-07-10 18:43:29.693229" 41 | }] 42 | } 43 | }, 44 | "sub_groups": {} 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /azdev/operations/tests/jsons/az_costmanagement_meta_after.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "costmanagement", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "costmanagement": { 7 | "name": "costmanagement", 8 | "commands": { 9 | "costmanagement show-operation-result": { 10 | "name": "costmanagement show-operation-result", 11 | "is_aaz": true, 12 | "supports_no_wait": true, 13 | "parameters": [{ 14 | "name": "no_wait", 15 | "options": ["--no-wait"], 16 | "choices": ["0", "1", "f", "false", "n", "no", "t", "true", "y", "yes"], 17 | "nargs": "?", 18 | "aaz_type": "bool", 19 | "type": "bool" 20 | }, { 21 | "name": "operation_id", 22 | "options": ["--operation-id"], 23 | "required": true, 24 | "aaz_type": "string", 25 | "type": "string" 26 | }, { 27 | "name": "scope", 28 | "options": ["--scope"], 29 | "required": true, 30 | "aaz_type": "string", 31 | "type": "string" 32 | }] 33 | } 34 | }, 35 | "sub_groups": { 36 | "costmanagement export": { 37 | "name": "costmanagement export", 38 | "commands": { 39 | "costmanagement export list": { 40 | "name": "costmanagement export list", 41 | "is_aaz": false, 42 | "parameters": [{ 43 | "name": "scope", 44 | "options": ["--scope"], 45 | "required": true 46 | }] 47 | }, 48 | "costmanagement export show": { 49 | "name": "costmanagement export show", 50 | "is_aaz": false, 51 | "parameters": [{ 52 | "name": "scope", 53 | "options": ["--scope"], 54 | "required": true 55 | }, { 56 | "name": "export_name", 57 | "options": ["--name"], 58 | "required": true 59 | }] 60 | }, 61 | "costmanagement export create": { 62 | "name": "costmanagement export create", 63 | "is_aaz": false, 64 | "parameters": [{ 65 | "name": "scope", 66 | "options": ["--scope"], 67 | "required": true 68 | }, { 69 | "name": "export_name", 70 | "options": ["--name"], 71 | "required": true 72 | }, { 73 | "name": "delivery_storage_container", 74 | "options": ["--storage-container"], 75 | "required": true 76 | }, { 77 | "name": "delivery_storage_account_id", 78 | "options": ["--storage-account-id"], 79 | "required": true 80 | }, { 81 | "name": "definition_timeframe", 82 | "options": ["--timeframe"], 83 | "required": true, 84 | "choices": ["BillingMonthToDate", "Custom", "MonthToDate", "TheLastBillingMonth", "TheLastMonth", "WeekToDate"] 85 | }, { 86 | "name": "delivery_directory", 87 | "options": ["--storage-directory"] 88 | }, { 89 | "name": "definition_type", 90 | "options": ["--type"], 91 | "choices": ["ActualCost", "AmortizedCost", "Usage"], 92 | "default": "Usage" 93 | }, { 94 | "name": "definition_time_period", 95 | "options": ["--time-period"], 96 | "nargs": "+" 97 | }, { 98 | "name": "definition_dataset_configuration", 99 | "options": ["--dataset-configuration"], 100 | "nargs": "+" 101 | }, { 102 | "name": "schedule_status", 103 | "options": ["--schedule-status"], 104 | "choices": ["Active", "Inactive"], 105 | "default": "Inactive" 106 | }, { 107 | "name": "schedule_recurrence", 108 | "options": ["--recurrence"], 109 | "choices": ["Annually", "Daily", "Monthly", "Weekly"] 110 | }, { 111 | "name": "schedule_recurrence_period", 112 | "options": ["--recurrence-period"], 113 | "nargs": "+" 114 | }] 115 | }, 116 | "costmanagement export delete": { 117 | "name": "costmanagement export delete", 118 | "is_aaz": false, 119 | "confirmation": true, 120 | "parameters": [{ 121 | "name": "scope", 122 | "options": ["--scope"], 123 | "required": true 124 | }, { 125 | "name": "export_name", 126 | "options": ["--name"], 127 | "required": true 128 | }, { 129 | "name": "yes", 130 | "options": ["--yes", "-y"] 131 | }] 132 | } 133 | }, 134 | "sub_groups": {} 135 | } 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /azdev/operations/tests/test_break_change.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | import unittest 9 | import os 10 | from azdev.operations.command_change import export_command_meta, cmp_command_meta 11 | from azdev.operations.command_change.util import get_command_tree, add_to_command_tree 12 | 13 | 14 | class BreakingChangeTestCase(unittest.TestCase): 15 | 16 | def test_cmd_meta_generation(self): 17 | if os.path.exists("./jsons/az_monitor_meta.json"): 18 | os.remove("./jsons/az_monitor_meta.json") 19 | module_list = ["monitor"] 20 | export_command_meta(modules=module_list, meta_output_path="./jsons/") 21 | self.assertTrue(os.path.exists("./jsons/az_monitor_meta.json"), "new monitor meta generation failed") 22 | 23 | def test_parse_cmd_tree(self): 24 | cmd_name = "monitor log-profiles create" 25 | ret = get_command_tree(cmd_name) 26 | self.assertTrue(ret["is_group"], "group parse failed") 27 | self.assertFalse(ret["sub_info"]["sub_info"]["is_group"], "group parse failed") 28 | self.assertTrue(ret["sub_info"]["sub_info"]["cmd_name"] == "monitor log-profiles create", "group parse failed") 29 | 30 | def test_diff_meta(self): 31 | if not os.path.exists("./jsons/az_monitor_meta_before.json") \ 32 | or not os.path.exists("./jsons/az_monitor_meta_after.json"): 33 | return 34 | result = cmp_command_meta(base_meta_file="./jsons/az_monitor_meta_before.json", 35 | diff_meta_file="./jsons/az_monitor_meta_after.json", 36 | output_type="text") 37 | target_message = [ 38 | "please confirm cmd `monitor private-link-scope scoped-resource show` removed", 39 | "sub group `monitor private-link-scope private-endpoint-connection cust` removed", 40 | ] 41 | for mes in target_message: 42 | found = False 43 | for line in result: 44 | if line.find(mes) > -1: 45 | found = True 46 | break 47 | self.assertTrue(found, "target message not found") 48 | 49 | ignored_message = [ 50 | "updated property `is_aaz` from `False` to `True`" 51 | ] 52 | for mes in ignored_message: 53 | ignored = True 54 | for line in result: 55 | if line.find(mes) > -1: 56 | ignored = False 57 | break 58 | self.assertTrue(ignored, "ignored message found") 59 | 60 | def test_command_tree(self): 61 | tree = {} 62 | add_to_command_tree(tree, 'a b c', 'd') 63 | add_to_command_tree(tree, 'a b foo', 'bar') 64 | add_to_command_tree(tree, 'a foo', 'baz') 65 | expected = { 66 | 'a': { 67 | 'b': { 68 | 'c': 'd', 69 | 'foo': 'bar' 70 | }, 71 | 'foo': 'baz' 72 | } 73 | } 74 | self.assertDictEqual(tree, expected) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /azdev/operations/tests/test_break_change_ci.sh: -------------------------------------------------------------------------------- 1 | [[ -e CLI ]] && rm -rf CLI 2 | azdev command-change meta-export CLI --meta-output-path ./CLI 3 | if [ $? -ne 0 ]; then 4 | echo "test cli meta generation failed, pass" 5 | else 6 | echo "test cli meta generation succeed" 7 | fi -------------------------------------------------------------------------------- /azdev/operations/tests/test_config.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import requests 9 | import yaml 10 | 11 | from knack.log import get_logger 12 | from azdev.utilities.path import get_cli_repo_path 13 | from azdev.operations.constant import ( 14 | ENCODING, GLOBAL_PARAMETERS, GENERIC_UPDATE_PARAMETERS, WAIT_CONDITION_PARAMETERS, OTHER_PARAMETERS, 15 | RED, ORANGE, GREEN, BLUE, GOLD, RED_PCT, ORANGE_PCT, GREEN_PCT, BLUE_PCT, CLI_OWN_MODULES, 16 | EXCLUDE_COMMANDS, GLOBAL_EXCLUDE_COMMANDS, EXCLUDE_MODULES, CMD_PATTERN, QUO_PATTERN, END_PATTERN, 17 | DOCS_END_PATTERN, NOT_END_PATTERN, NUMBER_SIGN_PATTERN) 18 | 19 | logger = get_logger(__name__) 20 | 21 | try: 22 | with open(os.path.join(get_cli_repo_path(), 'scripts', 'ci', 'cmdcov.yml'), 'r') as file: 23 | config = yaml.safe_load(file) 24 | # pylint: disable=broad-exception-caught 25 | except Exception: 26 | url = "https://raw.githubusercontent.com/Azure/azure-cli/dev/scripts/ci/cmdcov.yml" 27 | response = requests.get(url) 28 | config = yaml.safe_load(response.text) 29 | print(config) 30 | assert config['ENCODING'] == ENCODING 31 | assert config['GLOBAL_PARAMETERS'] == GLOBAL_PARAMETERS 32 | assert config['GENERIC_UPDATE_PARAMETERS'] == GENERIC_UPDATE_PARAMETERS 33 | assert config['WAIT_CONDITION_PARAMETERS'] == WAIT_CONDITION_PARAMETERS 34 | assert config['OTHER_PARAMETERS'] == OTHER_PARAMETERS 35 | assert config['RED'] == RED 36 | assert config['ORANGE'] == ORANGE 37 | assert config['GREEN'] == GREEN 38 | assert config['BLUE'] == BLUE 39 | assert config['GOLD'] == GOLD 40 | assert config['RED_PCT'] == RED_PCT 41 | assert config['ORANGE_PCT'] == ORANGE_PCT 42 | assert config['GREEN_PCT'] == GREEN_PCT 43 | assert config['BLUE_PCT'] == BLUE_PCT 44 | assert config['CLI_OWN_MODULES'] == CLI_OWN_MODULES 45 | assert config['EXCLUDE_COMMANDS'] == EXCLUDE_COMMANDS 46 | assert config['GLOBAL_EXCLUDE_COMMANDS'] == GLOBAL_EXCLUDE_COMMANDS 47 | assert config['EXCLUDE_MODULES'] == EXCLUDE_MODULES 48 | assert config['CMD_PATTERN'] == CMD_PATTERN 49 | assert config['QUO_PATTERN'] == QUO_PATTERN 50 | assert config['END_PATTERN'] == END_PATTERN 51 | assert config['DOCS_END_PATTERN'] == DOCS_END_PATTERN 52 | assert config['NOT_END_PATTERN'] == NOT_END_PATTERN 53 | assert config['NUMBER_SIGN_PATTERN'] == NUMBER_SIGN_PATTERN 54 | -------------------------------------------------------------------------------- /azdev/operations/tests/test_style.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import configparser 8 | import unittest 9 | from unittest import mock 10 | 11 | from azdev.operations.style import _config_file_path 12 | 13 | 14 | class TestConfigFilePath(unittest.TestCase): 15 | def test_unsupported_code_style_checker(self): 16 | with self.assertRaises(ValueError): 17 | _config_file_path(style_type="unknown") 18 | 19 | def test_pylint_config_without_setup(self): 20 | mocked_config = configparser.ConfigParser() 21 | mocked_config.add_section("cli") 22 | mocked_config.set("cli", "repo_path", "") 23 | mocked_config.add_section("ext") 24 | mocked_config.set("ext", "repo_paths", "") 25 | 26 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 27 | r = _config_file_path(style_type="pylint") 28 | self.assertTrue(r[0].endswith("/config_files/cli_pylintrc")) 29 | self.assertTrue(r[1].endswith("/config_files/ext_pylintrc")) 30 | 31 | def test_pylint_config_with_partially_setup(self): 32 | cli_repo_path = "/mnt/vss/_work/1/s/azure-cli" 33 | mocked_config = configparser.ConfigParser() 34 | mocked_config.add_section("cli") 35 | mocked_config.set("cli", "repo_path", cli_repo_path) 36 | mocked_config.add_section("ext") 37 | mocked_config.set("ext", "repo_paths", "") 38 | 39 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 40 | r = _config_file_path(style_type="pylint") 41 | self.assertEqual(r[0], cli_repo_path + "/pylintrc") 42 | self.assertTrue(r[1].endswith("/config_files/ext_pylintrc")) 43 | 44 | def test_pylint_config_with_all_setup(self): 45 | cli_repo_path = "/mnt/vss/_work/1/s/azure-cli" 46 | ext_repo_path = "/mnt/vss/_work/1/s/azure-cli-extensions" 47 | mocked_config = configparser.ConfigParser() 48 | mocked_config.add_section("cli") 49 | mocked_config.set("cli", "repo_path", cli_repo_path) 50 | mocked_config.add_section("ext") 51 | mocked_config.set("ext", "repo_paths", ext_repo_path) 52 | 53 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 54 | r = _config_file_path() 55 | self.assertEqual(r[0], cli_repo_path + "/pylintrc") 56 | self.assertEqual(r[1], ext_repo_path + "/pylintrc") 57 | 58 | def test_flake8_config_wihtout_setup(self): 59 | mocked_config = configparser.ConfigParser() 60 | mocked_config.add_section("cli") 61 | mocked_config.set("cli", "repo_path", "") 62 | mocked_config.add_section("ext") 63 | mocked_config.set("ext", "repo_paths", "") 64 | 65 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 66 | r = _config_file_path(style_type="flake8") 67 | self.assertTrue(r[0].endswith("/config_files/cli.flake8")) 68 | self.assertTrue(r[1].endswith("/config_files/ext.flake8")) 69 | 70 | def test_flake8_config_with_partially_setup(self): 71 | ext_repo_path = "/mnt/vss/_work/1/s/azure-cli-extensions" 72 | 73 | mocked_config = configparser.ConfigParser() 74 | mocked_config.add_section("cli") 75 | mocked_config.set("cli", "repo_path", "") 76 | mocked_config.add_section("ext") 77 | mocked_config.set("ext", "repo_paths", ext_repo_path) 78 | 79 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 80 | r = _config_file_path(style_type="flake8") 81 | self.assertTrue(r[0].endswith("/config_files/cli.flake8")) 82 | self.assertTrue(r[1].endswith(ext_repo_path + "/.flake8")) 83 | 84 | def test_flake8_config_with_all_setup(self): 85 | cli_repo_path = "/mnt/vss/_work/1/s/azure-cli" 86 | ext_repo_path = "/mnt/vss/_work/1/s/azure-cli-extensions" 87 | 88 | mocked_config = configparser.ConfigParser() 89 | mocked_config.add_section("cli") 90 | mocked_config.set("cli", "repo_path", cli_repo_path) 91 | mocked_config.add_section("ext") 92 | mocked_config.set("ext", "repo_paths", ext_repo_path) 93 | 94 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 95 | r = _config_file_path(style_type="flake8") 96 | self.assertTrue(r[0].endswith(cli_repo_path + "/.flake8")) 97 | self.assertTrue(r[1].endswith(ext_repo_path + "/.flake8")) 98 | 99 | def test_style_with_all_setup_but_not_exist(self): 100 | cli_repo_path = "/fake/azure-cli" 101 | ext_repo_path = "/fake/azure-cli-extensions" 102 | mocked_config = configparser.ConfigParser() 103 | mocked_config.add_section("cli") 104 | mocked_config.set("cli", "repo_path", cli_repo_path) 105 | mocked_config.add_section("ext") 106 | mocked_config.set("ext", "repo_paths", ext_repo_path) 107 | 108 | with mock.patch("azdev.operations.style.get_azdev_config", return_value=mocked_config): 109 | r1 = _config_file_path(style_type="flake8") 110 | r2 = _config_file_path(style_type="pylint") 111 | self.assertTrue(r1[0].endswith("/config_files/cli.flake8")) 112 | self.assertTrue(r1[1].endswith("/config_files/ext.flake8")) 113 | self.assertTrue(r2[0].endswith("/config_files/cli_pylintrc")) 114 | self.assertTrue(r2[1].endswith("/config_files/ext_pylintrc")) 115 | -------------------------------------------------------------------------------- /azdev/operations/testtool/incremental_strategy.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import abc 8 | from knack.util import CLIError 9 | 10 | from azdev.utilities import get_path_table, git_util 11 | 12 | 13 | # @wrapt.decorator 14 | # def cli_release_scenario(wrapped, _, args, kwargs): 15 | # """ 16 | # Filter out those files in Azure CLI release stage 17 | # """ 18 | # # TODO 19 | # # if instance.resolved: 20 | # # return instance 21 | 22 | # return wrapped(*args, **kwargs) 23 | 24 | 25 | class AzureDevOpsContext(abc.ABC): 26 | def __init__(self, git_repo, git_source, git_target): 27 | """ 28 | :param git_source: could be commit id, branch name or any valid value for git diff 29 | :param git_target: could be commit id, branch name or any valid value for git diff 30 | """ 31 | self.git_repo = git_repo 32 | self.git_source = git_source 33 | self.git_target = git_target 34 | 35 | @abc.abstractmethod 36 | def filter(self, test_index): 37 | pass 38 | 39 | 40 | class CLIAzureDevOpsContext(AzureDevOpsContext): 41 | """ 42 | Assemble strategy of incremental test on Azure DevOps Environment for Azure CLI 43 | """ 44 | def __init__(self, git_repo, git_source, git_target): 45 | super().__init__(git_repo, git_source, git_target) 46 | 47 | if not any([self.git_source, self.git_target, self.git_repo]): 48 | raise CLIError('usage error: [--src NAME] --tgt NAME --repo PATH --cli-ci') 49 | 50 | if not all([self.git_target, self.git_repo]): 51 | raise CLIError('usage error: [--src NAME] --tgt NAME --repo PATH --cli-ci') 52 | 53 | @property 54 | def modified_files(self): 55 | modified_files = git_util.diff_branches(self.git_repo, self.git_source, self.git_target) 56 | return [f for f in modified_files if f.startswith('src/')] 57 | 58 | def filter(self, test_index): 59 | """ 60 | Strategy on Azure CLI pull request verification stage. 61 | 62 | :return: a list of modified packages 63 | """ 64 | 65 | modified_packages = git_util.summarize_changed_mods(self.modified_files) 66 | 67 | if any(core_package in modified_packages for core_package in ['core', 'testsdk', 'telemetry']): 68 | path_table = get_path_table() 69 | 70 | # tests under all packages 71 | return list(path_table['mod'].keys()) + list(path_table['core'].keys()) + list(path_table['ext'].keys()) 72 | 73 | return modified_packages 74 | -------------------------------------------------------------------------------- /azdev/operations/testtool/profile_context.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import traceback 9 | 10 | from knack.log import get_logger 11 | from knack.util import CLIError 12 | 13 | from azdev.utilities import call, cmd 14 | from azdev.utilities import display 15 | 16 | 17 | logger = get_logger(__name__) 18 | os.environ['AZURE_CORE_COLLECT_TELEMETRY'] = 'False' 19 | 20 | 21 | class ProfileContext: 22 | def __init__(self, profile_name=None): 23 | self.target_profile = profile_name 24 | 25 | self.origin_profile = current_profile() 26 | 27 | def __enter__(self): 28 | if self.target_profile is None or self.target_profile == self.origin_profile: 29 | display('The tests are set to run against current profile "{}"'.format(self.origin_profile)) 30 | else: 31 | result = cmd('az cloud update --profile {}'.format(self.target_profile), 32 | 'Switching to target profile "{}"...'.format(self.target_profile)) 33 | if result.exit_code != 0: 34 | raise CLIError(result.error.output.decode('utf-8')) 35 | 36 | def __exit__(self, exc_type, exc_val, exc_tb): 37 | if self.target_profile is not None and self.target_profile != self.origin_profile: 38 | display('Switching back to origin profile "{}"...'.format(self.origin_profile)) 39 | call('az cloud update --profile {}'.format(self.origin_profile)) 40 | 41 | if exc_tb: 42 | display('') 43 | traceback.print_exception(exc_type, exc_val, exc_tb) 44 | 45 | 46 | def current_profile(): 47 | return cmd('az cloud show --query profile -otsv', show_stderr=False).result 48 | -------------------------------------------------------------------------------- /azdev/operations/testtool/pytest_runner.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import sys 9 | 10 | from knack.log import get_logger 11 | 12 | from azdev.utilities import call 13 | 14 | 15 | def get_test_runner(parallel, log_path, last_failed, no_exit_first, mark): 16 | """Create a pytest execution method""" 17 | def _run(test_paths, pytest_args): 18 | 19 | logger = get_logger(__name__) 20 | 21 | if os.name == 'posix': 22 | arguments = ['-x', '-v', '--forked', '-p no:warnings', '--log-level=WARN', '--junit-xml', log_path] 23 | else: 24 | arguments = ['-x', '-v', '-p no:warnings', '--log-level=WARN', '--junit-xml', log_path] 25 | 26 | if no_exit_first: 27 | arguments.remove('-x') 28 | 29 | if mark: 30 | arguments.append('-m "{}"'.format(mark)) 31 | 32 | arguments.extend(test_paths) 33 | if parallel: 34 | arguments += ['-n', 'auto'] 35 | if last_failed: 36 | arguments.append('--lf') 37 | if pytest_args: 38 | arguments += pytest_args 39 | cmd = sys.executable + ' -m pytest {}'.format(' '.join(arguments)) 40 | logger.info('Running: %s', cmd) 41 | return call(cmd) 42 | 43 | return _run 44 | -------------------------------------------------------------------------------- /azdev/operations/testtool/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azdev/operations/testtool/tests/test_profile_context.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import unittest 8 | 9 | from knack.util import CLIError 10 | 11 | from azdev.operations.testtool.profile_context import ProfileContext 12 | 13 | 14 | class TestProfileContext(unittest.TestCase): 15 | 16 | def test_profile_ok(self): 17 | target_profiles = ['latest'] 18 | 19 | for profile in target_profiles: 20 | with ProfileContext(profile): 21 | self.assertEqual(1, 1) 22 | 23 | def test_unsupported_profile(self): 24 | unknown_profile = 'unknown-profile' 25 | 26 | with self.assertRaises(CLIError): 27 | with ProfileContext(unknown_profile): 28 | pass 29 | 30 | def test_raise_inner_exception(self): 31 | with self.assertRaises(Exception): 32 | with ProfileContext('latest'): 33 | raise Exception('inner Exception') 34 | -------------------------------------------------------------------------------- /azdev/transformers.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | def performance_benchmark_data_transformer(result): 9 | from collections import OrderedDict 10 | 11 | output = [] 12 | 13 | for r in result: 14 | item = OrderedDict() 15 | item["Command"] = r["Command"] 16 | item["Min"] = r["Min"] 17 | item["Avg"] = r["Avg"] 18 | item["Max"] = r["Max"] 19 | item["Media"] = r["Media"] 20 | item["Std"] = r["Std"] 21 | output.append(item) 22 | 23 | return output 24 | -------------------------------------------------------------------------------- /azdev/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | from .config import ( 8 | get_azure_config, 9 | get_azure_config_dir, 10 | get_azdev_config, 11 | get_azdev_config_dir, 12 | ) 13 | from .command import ( 14 | call, 15 | cmd, 16 | py_cmd, 17 | pip_cmd, 18 | CommandError 19 | ) 20 | from .const import ( 21 | COMMAND_MODULE_PREFIX, 22 | EXTENSION_PREFIX, 23 | IS_WINDOWS, 24 | ENV_VAR_TEST_MODULES, 25 | ENV_VAR_TEST_LIVE, 26 | ENV_VAR_VIRTUAL_ENV, 27 | EXT_REPO_NAME 28 | ) 29 | from .display import ( 30 | display, 31 | output, 32 | heading, 33 | subheading 34 | ) 35 | from .git_util import ( 36 | diff_branches, 37 | filter_by_git_diff, 38 | diff_branch_file_patch, 39 | diff_branches_detail 40 | ) 41 | from .path import ( 42 | extract_module_name, 43 | find_file, 44 | find_files, 45 | make_dirs, 46 | get_env_path, 47 | get_azdev_repo_path, 48 | get_cli_repo_path, 49 | get_ext_repo_paths, 50 | get_path_table, 51 | get_name_index, 52 | calc_selected_mod_names 53 | ) 54 | from .testing import test_cmd 55 | from .tools import ( 56 | require_virtual_env, 57 | require_azure_cli 58 | ) 59 | 60 | 61 | __all__ = [ 62 | 'COMMAND_MODULE_PREFIX', 63 | 'EXTENSION_PREFIX', 64 | 'display', 65 | 'output', 66 | 'heading', 67 | 'subheading', 68 | 'diff_branches', 69 | 'filter_by_git_diff', 70 | 'call', 71 | 'cmd', 72 | 'py_cmd', 73 | 'pip_cmd', 74 | 'CommandError', 75 | 'test_cmd', 76 | 'get_env_path', 77 | 'get_azure_config_dir', 78 | 'get_azure_config', 79 | 'get_azdev_config_dir', 80 | 'get_azdev_config', 81 | 'ENV_VAR_TEST_MODULES', 82 | 'ENV_VAR_TEST_LIVE', 83 | 'ENV_VAR_VIRTUAL_ENV', 84 | 'EXT_REPO_NAME', 85 | 'IS_WINDOWS', 86 | 'extract_module_name', 87 | 'find_file', 88 | 'find_files', 89 | 'make_dirs', 90 | 'get_azdev_repo_path', 91 | 'get_cli_repo_path', 92 | 'get_ext_repo_paths', 93 | 'get_path_table', 94 | 'get_name_index', 95 | 'require_virtual_env', 96 | 'require_azure_cli', 97 | 'diff_branches_detail', 98 | 'diff_branch_file_patch', 99 | 'calc_selected_mod_names', 100 | ] 101 | -------------------------------------------------------------------------------- /azdev/utilities/command.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | import shlex 11 | 12 | from knack.log import get_logger 13 | from knack.util import CommandResultItem 14 | 15 | logger = get_logger(__name__) 16 | 17 | 18 | class CommandError(Exception): 19 | 20 | def __init__(self, output, exit_code, command): 21 | message = "Command `{}` failed with exit code {}:\n{}".format(command, exit_code, output) 22 | self.exit_code = exit_code 23 | self.output = output 24 | self.command = command 25 | super().__init__(message) 26 | 27 | 28 | def call(command, **kwargs): 29 | """ Run an arbitrary command but don't buffer the output. 30 | 31 | :param command: The entire command line to run. 32 | :param kwargs: Any kwargs supported by subprocess.Popen 33 | :returns: (int) process exit code. 34 | """ 35 | from azdev.utilities import IS_WINDOWS 36 | cmd_args = command 37 | if IS_WINDOWS and command.startswith('az '): 38 | cmd_args = "az.bat " + command[3:] 39 | if not IS_WINDOWS: 40 | cmd_args = shlex.split(command) 41 | return subprocess.run( 42 | cmd_args, 43 | check=False, # supress subprocess-run-check linter warning, no CalledProcessError 44 | **kwargs).returncode 45 | 46 | 47 | def cmd(command, message=False, show_stderr=True, raise_error=False, **kwargs): 48 | """ Run an arbitrary command. 49 | 50 | :param command: The entire command line to run. 51 | :param message: A custom message to display, or True (bool) to use a default. 52 | :param show_stderr: On error, display the contents of STDERR. 53 | :param raise_error: On error, raise CommandError. 54 | :param kwargs: Any kwargs supported by subprocess.Popen 55 | :returns: CommandResultItem object. 56 | """ 57 | from azdev.utilities import IS_WINDOWS, display 58 | 59 | # use default message if custom not provided 60 | if message is True: 61 | message = 'Running: {}\n'.format(command) 62 | 63 | if message: 64 | display(message) 65 | 66 | logger.info("Running: %s", command) 67 | cmd_args = command 68 | if IS_WINDOWS and command.startswith('az '): 69 | cmd_args = "az.bat " + command[3:] 70 | if not IS_WINDOWS: 71 | cmd_args = shlex.split(command) 72 | try: 73 | output = subprocess.run( 74 | cmd_args, 75 | check=True, 76 | stdout=subprocess.PIPE, 77 | stderr=subprocess.STDOUT if show_stderr else None, 78 | **kwargs).stdout.decode('utf-8').strip() 79 | logger.debug(output) 80 | return CommandResultItem(output, exit_code=0, error=None) 81 | except subprocess.CalledProcessError as err: 82 | if raise_error: 83 | raise CommandError(err.output.decode(), err.returncode, command) 84 | return CommandResultItem(err.output, exit_code=err.returncode, error=err) 85 | 86 | 87 | def py_cmd(command, message=False, show_stderr=True, raise_error=False, is_module=True, **kwargs): 88 | """ Run a script or command with Python. 89 | 90 | :param command: The arguments to run python with. 91 | :param message: A custom message to display, or True (bool) to use a default. 92 | :param show_stderr: On error, display the contents of STDERR. 93 | :param raise_error: On error, raise CommandError. 94 | :param is_module: Run a Python module as a script with -m. 95 | :param kwargs: Any kwargs supported by subprocess.Popen 96 | :returns: CommandResultItem object. 97 | """ 98 | from azdev.utilities import get_env_path 99 | env_path = get_env_path() 100 | python_bin = sys.executable if not env_path else os.path.join( 101 | env_path, 'Scripts' if sys.platform == 'win32' else 'bin', 'python') 102 | if is_module: 103 | command = '{} -m {}'.format(python_bin, command) 104 | else: 105 | command = '{} {}'.format(python_bin, command) 106 | return cmd(command, message, show_stderr, raise_error, **kwargs) 107 | 108 | 109 | def pip_cmd(command, message=False, show_stderr=True, raise_error=True, **kwargs): 110 | """ Run a pip command. 111 | 112 | :param command: The arguments to run pip with. 113 | :param message: A custom message to display, or True (bool) to use a default. 114 | :param show_stderr: On error, display the contents of STDERR. 115 | :param raise_error: On error, raise CommandError. As pip_cmd is usually called as a control function, instead of 116 | a test target, default to True. 117 | :param kwargs: Any kwargs supported by subprocess.Popen 118 | :returns: CommandResultItem object. 119 | """ 120 | command = 'pip {}'.format(command) 121 | return py_cmd(command, message, show_stderr, raise_error, **kwargs) 122 | -------------------------------------------------------------------------------- /azdev/utilities/config.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | 9 | from knack.config import CLIConfig 10 | 11 | 12 | def get_azdev_config(): 13 | return CLIConfig(config_dir=get_azdev_config_dir(), config_env_var_prefix='AZDEV') 14 | 15 | 16 | def get_azure_config(): 17 | return CLIConfig(config_dir=get_azure_config_dir(), config_env_var_prefix='AZURE') 18 | 19 | 20 | def get_azdev_config_dir(): 21 | """ Returns the user's .azdev directory. """ 22 | from azdev.utilities import get_env_path 23 | env_name = None 24 | _, env_name = os.path.splitdrive(get_env_path()) 25 | azdev_dir = os.getenv('AZDEV_CONFIG_DIR', None) or os.path.expanduser(os.path.join('~', '.azdev')) 26 | if not env_name: 27 | return azdev_dir 28 | return os.path.join(azdev_dir, 'env_config') + env_name 29 | 30 | 31 | def get_azure_config_dir(): 32 | """ Returns the user's Azure directory. """ 33 | return os.getenv('AZURE_CONFIG_DIR', None) or os.path.expanduser(os.path.join('~', '.azure')) 34 | -------------------------------------------------------------------------------- /azdev/utilities/const.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import sys 8 | 9 | COMMAND_MODULE_PREFIX = 'azure-cli-' 10 | EXTENSION_PREFIX = 'azext_' 11 | EXT_REPO_NAME = 'azure-cli-extensions' 12 | IS_WINDOWS = sys.platform.lower() in ['windows', 'win32'] 13 | 14 | ENV_VAR_TEST_MODULES = 'AZDEV_TEST_TESTS' # comma-separated list of modules to test 15 | ENV_VAR_VIRTUAL_ENV = ['VIRTUAL_ENV', 'CONDA_PREFIX'] # used by system to identify virtual environment 16 | ENV_VAR_TEST_LIVE = 'AZURE_TEST_RUN_LIVE' # denotes that tests should be run live instead of played back 17 | -------------------------------------------------------------------------------- /azdev/utilities/display.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import sys 8 | 9 | 10 | def display(txt): 11 | """ Output to stderr """ 12 | print(txt, file=sys.stderr) 13 | 14 | 15 | def output(txt, **kwargs): 16 | """ Output to stdout """ 17 | print(txt, file=sys.stdout, **kwargs) 18 | 19 | 20 | def heading(txt): 21 | """ Create standard heading to stderr """ 22 | line_len = len(txt) + 4 23 | display('\n' + '=' * line_len) 24 | display('| {} |'.format(txt)) 25 | display('=' * line_len + '\n') 26 | 27 | 28 | def subheading(txt): 29 | """ Create standard heading to stderr """ 30 | line_len = len(txt) + 2 31 | display('\n {} '.format(txt)) 32 | display('=' * line_len + '\n') 33 | -------------------------------------------------------------------------------- /azdev/utilities/git_util.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | 9 | from knack.log import get_logger 10 | from knack.util import CLIError 11 | 12 | logger = get_logger(__name__) 13 | 14 | 15 | def filter_by_git_diff(selected_modules, git_source, git_target, git_repo): 16 | if not any([git_source, git_target, git_repo]): 17 | return selected_modules 18 | 19 | if not all([git_target, git_repo]): 20 | raise CLIError('usage error: [--src NAME] --tgt NAME --repo PATH') 21 | 22 | files_changed = diff_branches(git_repo, git_target, git_source) 23 | mods_changed = summarize_changed_mods(files_changed) 24 | 25 | repo_path = str(os.path.abspath(git_repo)).lower() 26 | to_remove = {'mod': [], 'core': [], 'ext': []} 27 | for key in selected_modules: 28 | for name, path in selected_modules[key].items(): 29 | path = path.lower() 30 | if path.startswith(repo_path): 31 | if name in mods_changed: 32 | # has changed, so do not filter out 33 | continue 34 | # if not in the repo or has not changed, filter out 35 | to_remove[key].append(name) 36 | 37 | # remove the unchanged modules 38 | for key, value in to_remove.items(): 39 | for name in value: 40 | selected_modules[key].pop(name) 41 | logger.info('Filtered out: %s', to_remove) 42 | 43 | return selected_modules 44 | 45 | 46 | def summarize_changed_mods(files_changed): 47 | from azdev.utilities import extract_module_name 48 | 49 | mod_set = set() 50 | for f in files_changed: 51 | try: 52 | mod_name = extract_module_name(f) 53 | except CLIError: 54 | # some files aren't part of a module 55 | continue 56 | mod_set.add(mod_name) 57 | return list(mod_set) 58 | 59 | 60 | def diff_branches(repo, target, source): 61 | """ Returns a list of files that have changed in a given repo 62 | between two branches. """ 63 | try: 64 | import git # pylint: disable=unused-import,unused-variable 65 | import git.exc as git_exc 66 | import gitdb 67 | except ImportError as ex: 68 | raise CLIError(ex) 69 | 70 | from git import Repo 71 | try: 72 | git_repo = Repo(repo) 73 | except (git_exc.NoSuchPathError, git_exc.InvalidGitRepositoryError): 74 | raise CLIError('invalid git repo: {}'.format(repo)) 75 | 76 | def get_commit(branch): 77 | try: 78 | return git_repo.commit(branch) 79 | except gitdb.exc.BadName: 80 | raise CLIError('usage error, invalid branch: {}'.format(branch)) 81 | 82 | if source: 83 | source_commit = get_commit(source) 84 | else: 85 | source_commit = git_repo.head.commit 86 | target_commit = get_commit(target) 87 | 88 | logger.info('Filtering down to modules which have changed based on:') 89 | logger.info('cd %s', repo) 90 | logger.info('git --no-pager diff %s..%s --name-only -- .\n', target_commit, source_commit) 91 | 92 | diff_index = target_commit.diff(source_commit) 93 | return [diff.b_path for diff in diff_index] 94 | 95 | 96 | def diff_branches_detail(repo, target, source): 97 | """ Returns compare results of files that have changed in a given repo between two branches. 98 | Only focus on these files: _params.py, commands.py, test_*.py """ 99 | try: 100 | import git # pylint: disable=unused-import,unused-variable 101 | import git.exc as git_exc 102 | import gitdb 103 | except ImportError as ex: 104 | raise CLIError(ex) 105 | 106 | from git import Repo 107 | try: 108 | git_repo = Repo(repo) 109 | except (git_exc.NoSuchPathError, git_exc.InvalidGitRepositoryError): 110 | raise CLIError('invalid git repo: {}'.format(repo)) 111 | 112 | def get_commit(branch): 113 | try: 114 | return git_repo.commit(branch) 115 | except gitdb.exc.BadName: 116 | raise CLIError('usage error, invalid branch: {}'.format(branch)) 117 | 118 | if source: 119 | source_commit = get_commit(source) 120 | else: 121 | source_commit = git_repo.head.commit 122 | target_commit = get_commit(target) 123 | 124 | logger.info('Filtering down to modules which have changed based on:') 125 | logger.info('cd %s', repo) 126 | logger.info('git --no-pager diff %s..%s --name-only -- .\n', target_commit, source_commit) 127 | 128 | diff_index = target_commit.diff(source_commit) 129 | return diff_index 130 | 131 | 132 | def diff_branch_file_patch(repo, target, source): 133 | """ Returns compare results of files that have changed in a given repo between two branches. 134 | Only focus on these files: _params.py, commands.py, test_*.py """ 135 | try: 136 | import git # pylint: disable=unused-import,unused-variable 137 | import git.exc as git_exc 138 | import gitdb 139 | except ImportError as ex: 140 | raise CLIError(ex) 141 | 142 | from git import Repo 143 | try: 144 | git_repo = Repo(repo) 145 | except (git_exc.NoSuchPathError, git_exc.InvalidGitRepositoryError): 146 | raise CLIError('invalid git repo: {}'.format(repo)) 147 | 148 | def get_commit(branch): 149 | try: 150 | return git_repo.commit(branch) 151 | except gitdb.exc.BadName: 152 | raise CLIError('usage error, invalid branch: {}'.format(branch)) 153 | 154 | if source: 155 | source_commit = get_commit(source) 156 | else: 157 | source_commit = git_repo.head.commit 158 | target_commit = get_commit(target) 159 | 160 | logger.info('Filtering down to modules which have changed based on:') 161 | logger.info('cd %s', repo) 162 | logger.info('git --no-pager diff %s..%s --name-only -- .\n', target_commit, source_commit) 163 | 164 | diff_index = target_commit.diff(source_commit, create_patch=True) 165 | return diff_index 166 | -------------------------------------------------------------------------------- /azdev/utilities/pypi.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | try: 8 | import xmlrpclib 9 | except ImportError: 10 | import xmlrpc.client as xmlrpclib 11 | 12 | 13 | def is_available_on_pypi(module_name, module_version): 14 | client = xmlrpclib.ServerProxy('https://pypi.python.org/pypi') 15 | available_versions = client.package_releases(module_name, True) 16 | return module_version in available_versions 17 | -------------------------------------------------------------------------------- /azdev/utilities/testing.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | def test_cmd(args): 9 | from azdev.__main__ import main 10 | import sys 11 | 12 | sys.argv = [sys.executable] + args.split() 13 | return main() 14 | -------------------------------------------------------------------------------- /azdev/utilities/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-cli-dev-tools/5130a24020ca0a47055e914a58a4af67d9eb98ad/azdev/utilities/tests/__init__.py -------------------------------------------------------------------------------- /azdev/utilities/tests/test_path.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | import unittest 9 | import os 10 | 11 | from azdev.utilities import get_path_table 12 | 13 | 14 | class TestGetPathTable(unittest.TestCase): 15 | def setUp(self): 16 | self.path_table = get_path_table() 17 | 18 | def test_component(self): 19 | self.assertTrue('core' in self.path_table) 20 | self.assertTrue('ext' in self.path_table) 21 | self.assertTrue('mod' in self.path_table) 22 | 23 | def test_core_modules_directory_exist(self): 24 | if 'core' not in self.path_table: 25 | self.skipTest("No 'core' key in what get_path_table() return") 26 | 27 | core_modules = self.path_table['core'] 28 | for _, mod_path in core_modules.items(): 29 | self.assertTrue(os.path.isdir(mod_path)) 30 | 31 | def test_command_modules_directory_exist(self): 32 | if 'mod' not in self.path_table: 33 | self.skipTest("No 'mod' key in what get_path_table() return") 34 | 35 | command_modules = self.path_table['mod'] 36 | for _, mod_path in command_modules.items(): 37 | self.assertTrue(os.path.isdir(mod_path)) 38 | 39 | def test_extension_modules_directory_exist(self): 40 | if 'ext' not in self.path_table: 41 | self.skipTest("No 'ext' key in what get_path_table() return") 42 | 43 | if not self.path_table['ext']: 44 | self.skipTest("No extension modules installed by azdev") 45 | 46 | extension_moduels = self.path_table['ext'] 47 | for _, mod_path in extension_moduels.items(): 48 | self.assertTrue(os.path.isdir(mod_path)) 49 | 50 | 51 | if __name__ == '__main__': 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /azdev/utilities/tools.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | from knack.util import CLIError 8 | 9 | 10 | def require_virtual_env(): 11 | from azdev.utilities import get_env_path 12 | 13 | env = get_env_path() 14 | if not env: 15 | raise CLIError('This command can only be run from an active virtual environment.') 16 | 17 | 18 | def require_azure_cli(): 19 | try: 20 | import azure.cli.core # pylint: disable=unused-import, unused-variable 21 | except ImportError: 22 | raise CLIError('CLI is not installed. Run `azdev setup`.') 23 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Release History 4 | =============== 5 | 0.1.0 6 | ++++++ 7 | * Enable meta-diff with `module_name` excluded 8 | 9 | 0.0.9 10 | ++++++ 11 | * Use dynamic metadata whitelist 12 | 13 | 0.0.8 14 | ++++++ 15 | * Set `deepDiff` un-breaking version 16 | 17 | 0.0.7 18 | ++++++ 19 | * Remove unnecessary debug logs for meta comparison 20 | 21 | 0.0.6 22 | ++++++ 23 | * Add diff support for deprecate_info in subgroup, cmd, parameters and options 24 | 25 | 0.0.5 26 | ++++++ 27 | * Add `DiffLevel` to meta comparison 28 | * Downgrade change level according to warn list 29 | 30 | 0.0.4 31 | ++++++ 32 | * Add meta change parameter update warn list 33 | 34 | 0.0.3 35 | ++++++ 36 | * Add prop value for parameter add/remove property 37 | * Remove type add break justification 38 | 39 | 0.0.2 40 | ++++++ 41 | * Change time consuming into info log level 42 | * Adjust pkg name according to https://peps.python.org/pep-0008/#package-and-module-names 43 | 44 | 0.0.1 45 | ++++++ 46 | * Initial release 47 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/README.rst: -------------------------------------------------------------------------------- 1 | Microsoft Azure CLI Diff Tools (azure-cli-diff-tool) 2 | ======================================================= 3 | 4 | The ``azure-cli-diff-tool`` is designed to aid azure-cli users in diffing metadata files to see its updates through historical versions for Azure CLI command modules and extensions. 5 | 6 | Setting up your environment 7 | +++++++++++++++++++++++++++++++++++++++ 8 | 9 | 1. Install Python 3.6+ from http://python.org. Please note that the version of Python that comes preinstalled on OSX is 2.7. 10 | 11 | 3. Create a new virtual environment for Python in the root of your clone. You can do this by running: 12 | 13 | Python 3.6+ (all platforms): 14 | 15 | :: 16 | 17 | python -m venv env 18 | 19 | or: 20 | 21 | :: 22 | 23 | python3 -m venv env 24 | 25 | 26 | 4. Activate the env virtual environment by running: 27 | 28 | Windows CMD.exe: 29 | 30 | :: 31 | 32 | env\scripts\activate.bat 33 | 34 | Windows Powershell: 35 | 36 | :: 37 | 38 | env\scripts\activate.ps1 39 | 40 | 41 | OSX/Linux (bash): 42 | 43 | :: 44 | 45 | source env/bin/activate 46 | 47 | 5. Install ``azure-cli-diff-tool`` by running: 48 | 49 | :: 50 | 51 | pip install azure-cli-diff-tool 52 | 53 | Reporting issues and feedback 54 | +++++++++++++++++++++++++++++ 55 | 56 | If you encounter any bugs with the tool please file an issue in the `Issues `__ section of our GitHub repo. 57 | 58 | Contribute Code 59 | +++++++++++++++ 60 | 61 | This project has adopted the `Microsoft Open Source Code of Conduct `__. 62 | 63 | For more information see the `Code of Conduct FAQ `__ or contact `opencode@microsoft.com `__ with any additional questions or comments. 64 | 65 | If you would like to become an active contributor to this project please 66 | follow the instructions provided in `Microsoft Azure Projects Contribution Guidelines `__. 67 | 68 | License 69 | +++++++ 70 | 71 | :: 72 | 73 | Azure CLI Diff Tools (azure-cli-diff-tool) 74 | 75 | Copyright (c) Microsoft Corporation 76 | All rights reserved. 77 | 78 | MIT License 79 | 80 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 81 | 82 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 83 | 84 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.:: 85 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/azure_cli_diff_tool/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | 8 | # pylint: disable=too-many-lines 9 | # pylint: disable=too-many-statements 10 | import os 11 | import json 12 | import time 13 | from enum import Enum 14 | import logging 15 | from deepdiff import DeepDiff 16 | from .meta_change_detect import MetaChangeDetect 17 | from .utils import get_blob_config, load_blob_config_file, get_target_version_modules, get_target_version_module, \ 18 | extract_module_name_from_meta_file, export_meta_changes_to_csv, export_meta_changes_to_json, \ 19 | export_meta_changes_to_dict, expand_deprecate_obj 20 | 21 | __VERSION__ = '0.1.0' 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class DiffExportFormat(Enum): 27 | DICT = "dict" 28 | TEXT = "text" 29 | TREE = "tree" 30 | 31 | 32 | def diff_export_format_choices(): 33 | return [form.value for form in DiffExportFormat] 34 | 35 | 36 | def check_meta_tool_compatibility(meta_version): 37 | if not meta_version: 38 | return False 39 | meta_version_vec = meta_version.split(".") 40 | tool_version_vec = __VERSION__.split(".") 41 | version_outdated = False 42 | for ind, v in enumerate(meta_version_vec): 43 | if v.isdigit() and tool_version_vec[ind].isdigit() and int(v) > int(tool_version_vec[ind]): 44 | version_outdated = True 45 | return version_outdated 46 | 47 | 48 | def meta_diff(base_meta_file, diff_meta_file, only_break=False, output_type="text", output_file=None): 49 | if not os.path.exists(base_meta_file): 50 | raise Exception("base meta file needed") 51 | if not os.path.exists(diff_meta_file): 52 | raise Exception("diff meta file needed") 53 | 54 | with open(base_meta_file, "r") as g: 55 | command_tree_before = json.load(g) 56 | with open(diff_meta_file, "r") as g: 57 | command_tree_after = json.load(g) 58 | if command_tree_before.get("compat_version", None) \ 59 | and check_meta_tool_compatibility(command_tree_before["compat_version"]): 60 | raise Exception("Please update your azure cli diff tool") 61 | if command_tree_after.get("compat_version", None) \ 62 | and check_meta_tool_compatibility(command_tree_after["compat_version"]): 63 | raise Exception("Please update your azure cli diff tool") 64 | expand_deprecate_obj(command_tree_before) 65 | expand_deprecate_obj(command_tree_after) 66 | diff = DeepDiff(command_tree_before, command_tree_after, exclude_paths=["root['module_name']"]) 67 | if not diff: 68 | print(f"No meta diffs from {diff_meta_file} to {base_meta_file}") 69 | return export_meta_changes_to_json(None, output_file) 70 | else: 71 | detected_changes = MetaChangeDetect(diff, command_tree_before, command_tree_after) 72 | detected_changes.check_deep_diffs() 73 | result = detected_changes.export_meta_changes(only_break, output_type) 74 | return export_meta_changes_to_json(result, output_file) 75 | 76 | 77 | def version_diff(base_version, diff_version, only_break=False, version_diff_file=None, use_cache=False, 78 | output_type="dict", target_module=None): 79 | config = load_blob_config_file() 80 | blob_url, path_prefix, index_file = get_blob_config(config) 81 | download_base_start = time.time() 82 | if target_module: 83 | base_version_module_list = get_target_version_module(blob_url, path_prefix, base_version, 84 | target_module, use_cache) 85 | else: 86 | base_version_module_list = get_target_version_modules(blob_url, path_prefix, index_file, base_version, use_cache) 87 | download_base_end = time.time() 88 | logger.info("base version {} meta files download using {} sec".format(base_version, 89 | download_base_end - download_base_start)) 90 | if target_module: 91 | get_target_version_module(blob_url, path_prefix, diff_version, 92 | target_module, use_cache) 93 | else: 94 | get_target_version_modules(blob_url, path_prefix, index_file, diff_version, use_cache) 95 | download_target_end = time.time() 96 | logger.info("diff version {} meta files download using {} sec".format(diff_version, 97 | download_target_end - download_base_end)) 98 | version_diffs = [] 99 | for _, base_meta_file_full_path, base_meta_file in base_version_module_list: 100 | module_name = extract_module_name_from_meta_file(base_meta_file) 101 | if not module_name: 102 | continue 103 | if target_module and module_name != target_module: 104 | continue 105 | diff_meta_file_full_path = os.path.join(os.getcwd(), path_prefix + diff_version, base_meta_file) 106 | if not os.path.exists(diff_meta_file_full_path): 107 | print(f"Module {module_name} removed for {diff_version}") 108 | continue 109 | with open(base_meta_file_full_path, "r") as g: 110 | command_tree_before = json.load(g) 111 | with open(diff_meta_file_full_path, "r") as g: 112 | command_tree_after = json.load(g) 113 | 114 | if command_tree_before.get("compat_version", None) \ 115 | and check_meta_tool_compatibility(command_tree_before["compat_version"]): 116 | raise Exception("Please update your azure cli diff tool") 117 | if command_tree_after.get("compat_version", None) \ 118 | and check_meta_tool_compatibility(command_tree_after["compat_version"]): 119 | raise Exception("Please update your azure cli diff tool") 120 | expand_deprecate_obj(command_tree_before) 121 | expand_deprecate_obj(command_tree_after) 122 | diff = DeepDiff(command_tree_before, command_tree_after) 123 | if not diff: 124 | print(f"No meta diffs from version: {diff_version}/{base_meta_file} for module: {module_name}") 125 | continue 126 | detected_changes = MetaChangeDetect(diff, command_tree_before, command_tree_after) 127 | detected_changes.check_deep_diffs() 128 | diff_objs = detected_changes.export_meta_changes(only_break, "dict") 129 | mod_obj = {"module": module_name} 130 | for obj in diff_objs: 131 | obj.update(mod_obj) 132 | version_diffs.append(obj) 133 | meta_change_end = time.time() 134 | logger.info("meta file diffs using {} sec".format(meta_change_end - download_target_end)) 135 | if output_type == "dict": 136 | return export_meta_changes_to_dict(version_diffs, version_diff_file) 137 | return export_meta_changes_to_csv(version_diffs, version_diff_file) 138 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/azure_cli_diff_tool/_const.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | BLOB_SETTING_CONFIG_FILE = "./data/blob_config.ini" 9 | script_directory = os.path.dirname(os.path.realpath(__file__)) 10 | CONFIG_FILE_PATH = f"{script_directory}/{BLOB_SETTING_CONFIG_FILE}" 11 | 12 | META_CHANDE_WHITELIST_FILE = "./data/meta_change_whitelist.txt" 13 | META_CHANDE_WHITELIST_FILE_PATH = f"{script_directory}/{META_CHANDE_WHITELIST_FILE}" 14 | META_CHANDE_WHITELIST_FILE_URL = "https://azcmdchangemgmt.blob.core.windows.net/azure-cli-diff-tool-config/meta_change_whitelist.txt" 15 | DOWNLOAD_THREADS = 30 16 | 17 | BREAKING_CHANE_RULE_LINK_URL_PREFIX = "https://github.com/Azure/azure-cli/blob/dev/doc/breaking_change_rules/" 18 | BREAKING_CHANE_RULE_LINK_URL_SUFFIX = ".md" 19 | 20 | CMD_REMOVE_SUFFIX_WARN_LIST = ["wait"] 21 | 22 | SUBGROUP_PROPERTY_REMOVE_BREAK_LIST = [] 23 | SUBGROUP_PROPERTY_REMOVE_WARN_LIST = ["deprecate_info_redirect"] 24 | 25 | SUBGROUP_PROPERTY_ADD_BREAK_LIST = [] 26 | SUBGROUP_PROPERTY_ADD_WARN_LIST = ["deprecate_info_hide", "deprecate_info_expiration"] 27 | 28 | SUBGROUP_PROPERTY_UPDATE_BREAK_LIST = [] 29 | SUBGROUP_PROPERTY_UPDATE_WARN_LIST = ["deprecate_info_expiration"] 30 | 31 | SUBGROUP_PROPERTY_IGNORED_LIST = [] 32 | 33 | CMD_PROPERTY_REMOVE_BREAK_LIST = [] 34 | CMD_PROPERTY_REMOVE_WARN_LIST = ["deprecate_info_redirect"] 35 | 36 | CMD_PROPERTY_ADD_BREAK_LIST = ["confirmation"] 37 | CMD_PROPERTY_ADD_WARN_LIST = ["deprecate_info_hide", "deprecate_info_expiration"] 38 | 39 | CMD_PROPERTY_UPDATE_BREAK_LIST = [] 40 | CMD_PROPERTY_UPDATE_WARN_LIST = ["deprecate_info_expiration", "deprecate_info_redirect"] 41 | 42 | CMD_PROPERTY_IGNORED_LIST = ["is_aaz", "supports_no_wait"] 43 | 44 | PARA_PROPERTY_REMOVE_BREAK_LIST = ["options"] 45 | PARA_PROPERTY_REMOVE_WARN_LIST = ["id_part", "nargs", "deprecate_info_redirect"] 46 | 47 | PARA_PROPERTY_ADD_BREAK_LIST = ["required"] 48 | PARA_PROPERTY_ADD_WARN_LIST = ["choices", "deprecate_info_expiration", "deprecate_info_hide", "options_deprecate_info"] 49 | 50 | PARA_PROPERTY_UPDATE_BREAK_LIST = ["default", "aaz_default"] 51 | PARA_PROPERTY_UPDATE_WARN_LIST = ["type", "aaz_type", "choices", "nargs", 52 | "deprecate_info_expiration", "deprecate_info_redirect", "options_deprecate_info"] 53 | 54 | PARA_NAME_IGNORED_LIST = ["force_string"] 55 | PARA_PROPERTY_IGNORED_LIST = [] 56 | PARA_VALUE_IGNORED_LIST = ["generic_update_set", "generic_update_add", "generic_update_remove", 57 | "generic_update_force_string"] 58 | 59 | EXPORTED_CSV_META_HEADER = ["module", "cmd_name", "rule_id", "rule_name", "is_break", "diff_level", 60 | "rule_link_url", "rule_message", "suggest_message"] 61 | 62 | CHANGE_RULE_MESSAGE_MAPPING = { 63 | "1000": "default Message", 64 | "1001": "cmd `{0}` added", 65 | "1002": "cmd `{0}` removed", 66 | "1003": "cmd `{0}` added property `{1}`", 67 | "1004": "cmd `{0}` removed property `{1}`", 68 | "1005": "cmd `{0}` updated property `{1}` from `{2}` to `{3}`", 69 | "1006": "cmd `{0}` added parameter `{1}`", 70 | "1007": "cmd `{0}` removed parameter `{1}`", 71 | "1008": "cmd `{0}` update parameter `{1}`: added property `{2}={3}`", 72 | "1009": "cmd `{0}` update parameter `{1}`: removed property `{2}={3}`", 73 | "1010": "cmd `{0}` update parameter `{1}`: updated property `{2}` from `{3}` to `{4}`", 74 | "1011": "sub group `{0}` added", 75 | "1012": "sub group `{0}` removed", 76 | "1013": "sub group `{0}` added property `{1}`", 77 | "1014": "sub group `{0}` removed property `{1}`", 78 | "1015": "sub group `{0}` updated property `{1}` from `{2}` to `{3}`", 79 | } 80 | 81 | CHANGE_SUGGEST_MESSAGE_MAPPING = { 82 | "1000": "default Message", 83 | "1001": "please confirm cmd `{0}` added", 84 | "1002": "please confirm cmd `{0}` removed", 85 | "1003": "please remove property `{0}` for cmd `{1}`", 86 | "1004": "please add back property `{0}` for cmd `{1}`", 87 | "1005": "please change property `{0}` from `{1}` to `{2}` for cmd `{3}`", 88 | "1006": "please remove parameter `{0}` for cmd `{1}`", 89 | "1007": "please add back parameter `{0}` for cmd `{1}`", 90 | "1008": "please remove property `{0}={1}` for parameter `{2}` of cmd `{3}`", 91 | "1009": "please add back property `{0}={1}` for parameter `{2}` of cmd `{3}`", 92 | "1010": "please change property `{0}` from `{1}` to `{2}` for parameter `{3}` of cmd `{4}`", 93 | "1011": "please confirm sub group `{0}` added", 94 | "1012": "please confirm sub group `{0}` removed", 95 | "1013": "please remove property `{0}` for sub group `{1}`", 96 | "1014": "please add back property `{0}` for sub group `{1}`", 97 | "1015": "please change property `{0}` from `{1}` to `{2}` for sub group `{3}`", 98 | } 99 | 100 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/azure_cli_diff_tool/data/blob_config.ini: -------------------------------------------------------------------------------- 1 | [BLOB] 2 | primary_endpoint=https://azcmdchangemgmt.blob.core.windows.net/cmd-metadata-per-version 3 | metadata_path_prefix=azure-cli- 4 | metadata_module_index_file=index.txt 5 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/azure_cli_diff_tool/data/meta_change_whitelist.txt: -------------------------------------------------------------------------------- 1 | 1010 ams asset get-sas-urls expiry_time default 2 | 1010 mariadb server create administrator_login default 3 | 1010 mysql flexible-server create administrator_login default 4 | 1010 mysql flexible-server restore restore_point_in_time default 5 | 1010 mysql flexible-server import create administrator_login default 6 | 1010 mysql flexible-server maintenance reschedule maintenance_start_time default 7 | 1010 mysql server create administrator_login default 8 | 1010 postgres flexible-server create administrator_login default 9 | 1010 postgres flexible-server restore restore_point_in_time default 10 | 1010 postgres server create administrator_login default -------------------------------------------------------------------------------- /azure-cli-diff-tool/pyproject.toml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | [build-system] 8 | # Minimum requirements for the build system to execute. 9 | requires = ["setuptools", "wheel"] 10 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) Microsoft Corporation. All rights reserved. 5 | # Licensed under the MIT License. See License.txt in the project root for 6 | # license information. 7 | # ----------------------------------------------------------------------------- 8 | 9 | """Azure Command Diff Tools package that can be installed using setuptools""" 10 | import os 11 | import re 12 | from setuptools import setup, find_packages 13 | 14 | diff_tool_path = os.path.dirname(os.path.realpath(__file__)) 15 | with open(os.path.join(diff_tool_path, 'azure_cli_diff_tool', '__init__.py'), 'r') as version_file: 16 | __VERSION__ = re.search(r'^__VERSION__\s*=\s*[\'"]([^\'"]*)[\'"]', 17 | version_file.read(), re.MULTILINE).group(1) 18 | 19 | with open('README.rst', 'r', encoding='utf-8') as f: 20 | README = f.read() 21 | with open('HISTORY.rst', 'r', encoding='utf-8') as f: 22 | HISTORY = f.read() 23 | 24 | setup(name="azure-cli-diff-tool", 25 | version=__VERSION__, 26 | description="A tool for cli metadata management", 27 | long_description=README + '\n\n' + HISTORY, 28 | license='MIT', 29 | author='Microsoft Corporation', 30 | author_email='azpycli@microsoft.com', 31 | packages=find_packages(), 32 | include_package_data=True, 33 | install_requires=["deepdiff==6.3.0", "requests~=2.32.3"], 34 | package_data={ 35 | "azure_cli_diff_tool": ["data/*"] 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /azure-cli-diff-tool/tests/jsons/az_acr_meta_after.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "acr", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "acr": { 7 | "name": "acr", 8 | "commands": {}, 9 | "sub_groups": { 10 | "acr helm": { 11 | "name": "acr helm", 12 | "commands": { 13 | "acr helm list": { 14 | "name": "acr helm list", 15 | "is_aaz": false, 16 | "parameters": [{ 17 | "name": "registry_name", 18 | "options": ["--name", "-n"], 19 | "required": true 20 | }, { 21 | "name": "repository", 22 | "options": ["--repository"], 23 | "default": "repo" 24 | }, { 25 | "name": "resource_group_name", 26 | "deprecate_info": { 27 | "redirect": "resource_group_name2", 28 | "hide": true 29 | }, 30 | "options": ["--resource-group", "-g"], 31 | "id_part": "resource_group" 32 | }, { 33 | "name": "tenant_suffix", 34 | "options": ["--suffix"] 35 | }, { 36 | "name": "username", 37 | "options": ["--username", "-u"] 38 | }, { 39 | "name": "password", 40 | "options": ["--password", "-p"], 41 | "options_deprecate_info": [{ 42 | "target": "--password", 43 | "redirect": "--registry-password", 44 | "hide": true 45 | }] 46 | }] 47 | }, 48 | "acr helm show": { 49 | "name": "acr helm show", 50 | "is_aaz": false, 51 | "deprecate_info": { 52 | "target": "acr helm show", 53 | "redirect": "acr helm show3", 54 | "hide": true 55 | }, 56 | "parameters": [{ 57 | "name": "registry_name", 58 | "options": ["--name", "-n"], 59 | "required": true 60 | }, { 61 | "name": "chart", 62 | "options": [], 63 | "required": true 64 | }, { 65 | "name": "version", 66 | "options": ["--version"] 67 | }, { 68 | "name": "repository", 69 | "options": ["--repository"], 70 | "default": "repo", 71 | "options_deprecate_info": [{ 72 | "redirect": "--repository3", 73 | "hide": true 74 | }] 75 | }, { 76 | "name": "resource_group_name", 77 | "deprecate_info": { 78 | "target": "resource_group_name", 79 | "hide": true 80 | }, 81 | "options": ["--resource-group", "-g"], 82 | "id_part": "resource_group" 83 | }, { 84 | "name": "tenant_suffix", 85 | "options": ["--suffix"] 86 | }, { 87 | "name": "username", 88 | "options": ["--username", "-u"] 89 | }, { 90 | "name": "password", 91 | "options": ["--password", "-p"] 92 | }] 93 | } 94 | }, 95 | "sub_groups": {} 96 | } 97 | }, 98 | "deprecate_info": { 99 | "target": "acr", 100 | "redirect": "acr3", 101 | "hide": true 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /azure-cli-diff-tool/tests/jsons/az_acr_meta_before.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "acr", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "acr": { 7 | "name": "acr", 8 | "commands": {}, 9 | "sub_groups": { 10 | "acr helm": { 11 | "name": "acr helm", 12 | "commands": { 13 | "acr helm list": { 14 | "name": "acr helm list", 15 | "is_aaz": false, 16 | "deprecate_info": { 17 | "target": "acr helm list", 18 | "redirect": "acr helm list2", 19 | "hide": true 20 | }, 21 | "parameters": [{ 22 | "name": "registry_name", 23 | "options": ["--name", "-n"], 24 | "required": true 25 | }, { 26 | "name": "repository", 27 | "options": ["--repository"], 28 | "options_deprecate_info": [{ 29 | "target": "--docker-registry-server-password", 30 | "redirect": "--registry-password" 31 | }], 32 | "default": "repo" 33 | }, { 34 | "name": "resource_group_name", 35 | "deprecate_info": { 36 | "target": "resource_group_name", 37 | "hide": true 38 | }, 39 | "options": ["--resource-group", "-g"], 40 | "id_part": "resource_group" 41 | }, { 42 | "name": "tenant_suffix", 43 | "options": ["--suffix"], 44 | "deprecate_info": { 45 | "target": "tenant_suffix", 46 | "hide": true 47 | } 48 | }, { 49 | "name": "username", 50 | "options": ["--username", "-u"] 51 | }, { 52 | "name": "password", 53 | "options": ["--password", "-p"], 54 | "options_deprecate_info": [{ 55 | "target": "--password", 56 | "redirect": "--registry-password", 57 | "hide": true 58 | }, { 59 | "target": "-p", 60 | "redirect": "--registry-p", 61 | "hide": true 62 | }] 63 | }] 64 | }, 65 | "acr helm show": { 66 | "name": "acr helm show", 67 | "is_aaz": false, 68 | "deprecate_info": { 69 | "target": "acr helm show", 70 | "redirect": "acr helm show2" 71 | }, 72 | "parameters": [{ 73 | "name": "registry_name", 74 | "options": ["--name", "-n"], 75 | "required": true 76 | }, { 77 | "name": "chart", 78 | "options": [], 79 | "required": true 80 | }, { 81 | "name": "version", 82 | "options": ["--version"], 83 | "options_deprecate_info": [{ 84 | "target": "--version", 85 | "redirect": "--version2" 86 | }] 87 | }, { 88 | "name": "repository", 89 | "options": ["--repository"], 90 | "default": "repo", 91 | "options_deprecate_info": [{ 92 | "target": "--repository", 93 | "redirect": "--repository2" 94 | }] 95 | }, { 96 | "name": "resource_group_name", 97 | "deprecate_info": { 98 | "target": "resource_group_name", 99 | "hide": true 100 | }, 101 | "options": ["--resource-group", "-g"], 102 | "id_part": "resource_group" 103 | }, { 104 | "name": "tenant_suffix", 105 | "options": ["--suffix"] 106 | }, { 107 | "name": "username", 108 | "options": ["--username", "-u"] 109 | }, { 110 | "name": "password", 111 | "options": ["--password", "-p"] 112 | }] 113 | } 114 | }, 115 | "sub_groups": {}, 116 | "deprecate_info": { 117 | "target": "acr helm", 118 | "redirect": "helm v3" 119 | } 120 | } 121 | }, 122 | "deprecate_info": { 123 | "target": "acr", 124 | "redirect": "acr2" 125 | } 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /azure-cli-diff-tool/tests/jsons/az_ams_meta_after.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "ams", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "ams": { 7 | "name": "ams", 8 | "commands": {}, 9 | "sub_groups": { 10 | "ams asset": { 11 | "name": "ams asset", 12 | "commands": { 13 | "ams asset get-sas-urls": { 14 | "name": "ams asset get-sas-urls", 15 | "is_aaz": false, 16 | "parameters": [{ 17 | "name": "resource_group_name", 18 | "options": ["--resource-group", "-g"], 19 | "required": true, 20 | "id_part": "resource_group" 21 | }, { 22 | "name": "account_name", 23 | "options": ["--account-name", "-a"], 24 | "required": true, 25 | "id_part": "name" 26 | }, { 27 | "name": "asset_name", 28 | "options": ["--name", "-n"], 29 | "required": true, 30 | "id_part": "child_name_1" 31 | }, { 32 | "name": "permissions", 33 | "options": ["--permissions"], 34 | "choices": ["Read", "ReadWrite", "ReadWriteDelete"], 35 | "default": "Read" 36 | }, { 37 | "name": "expiry_time", 38 | "options": ["--expiry"], 39 | "type": "custom_type", 40 | "default": "2023-07-10 18:47:05.586776" 41 | }] 42 | } 43 | }, 44 | "sub_groups": {} 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /azure-cli-diff-tool/tests/jsons/az_ams_meta_before.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_name": "ams", 3 | "name": "az", 4 | "commands": {}, 5 | "sub_groups": { 6 | "ams": { 7 | "name": "ams", 8 | "commands": {}, 9 | "sub_groups": { 10 | "ams asset": { 11 | "name": "ams asset", 12 | "commands": { 13 | "ams asset get-sas-urls": { 14 | "name": "ams asset get-sas-urls", 15 | "is_aaz": false, 16 | "parameters": [{ 17 | "name": "resource_group_name", 18 | "options": ["--resource-group", "-g"], 19 | "required": true, 20 | "id_part": "resource_group" 21 | }, { 22 | "name": "account_name", 23 | "options": ["--account-name", "-a"], 24 | "required": true, 25 | "id_part": "name" 26 | }, { 27 | "name": "asset_name", 28 | "options": ["--name", "-n"], 29 | "required": true, 30 | "id_part": "child_name_1" 31 | }, { 32 | "name": "permissions", 33 | "options": ["--permissions"], 34 | "choices": ["Read", "ReadWrite", "ReadWriteDelete"], 35 | "default": "Read" 36 | }, { 37 | "name": "expiry_time", 38 | "options": ["--expiry"], 39 | "type": "custom_type", 40 | "default": "2023-07-10 18:43:29.693229" 41 | }] 42 | } 43 | }, 44 | "sub_groups": {} 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | - repo: self 3 | trigger: 4 | batch: true 5 | branches: 6 | include: 7 | - '*' 8 | jobs: 9 | - job: Tox 10 | condition: succeeded() 11 | pool: 12 | name: 'pool-ubuntu-2004' 13 | strategy: 14 | matrix: 15 | Python39: 16 | python.version: '3.9' 17 | tox_env: 'py39' 18 | Python310: 19 | python.version: '3.10' 20 | tox_env: 'py310' 21 | Python311: 22 | python.version: '3.11' 23 | tox_env: 'py311' 24 | Python312: 25 | python.version: '3.12' 26 | tox_env: 'py312' 27 | steps: 28 | - task: UsePythonVersion@0 29 | displayName: 'Use Python $(python.version)' 30 | inputs: 31 | versionSpec: '$(python.version)' 32 | - task: Bash@3 33 | displayName: 'Run Tox' 34 | env: 35 | TOXENV: $(tox_env) 36 | inputs: 37 | targetType: 'filePath' 38 | filePath: scripts/ci/run_tox.sh 39 | 40 | - job: ExtractMetadata 41 | displayName: 'Extract Metadata' 42 | condition: succeeded() 43 | pool: 44 | name: 'pool-ubuntu-2004' 45 | steps: 46 | - task: Bash@3 47 | displayName: 'Extract Version' 48 | inputs: 49 | targetType: 'filePath' 50 | filePath: scripts/ci/extract_version.sh 51 | 52 | - task: PublishPipelineArtifact@0 53 | displayName: 'Publish Artifact: metadata' 54 | inputs: 55 | TargetPath: $(Build.ArtifactStagingDirectory) 56 | ArtifactName: metadata 57 | 58 | - job: BuildPythonWheel 59 | displayName: 'Build Python Wheel' 60 | 61 | dependsOn: ExtractMetadata 62 | condition: succeeded() 63 | pool: 64 | name: 'pool-ubuntu-2004' 65 | steps: 66 | - task: UsePythonVersion@0 67 | displayName: 'Use Python 3.9' 68 | inputs: 69 | versionSpec: 3.9 70 | 71 | - task: Bash@3 72 | displayName: 'Run Wheel Build Script' 73 | inputs: 74 | targetType: 'filePath' 75 | filePath: scripts/ci/build.sh 76 | 77 | - task: PublishPipelineArtifact@0 78 | displayName: 'Publish Artifact: pypi' 79 | inputs: 80 | TargetPath: $(Build.ArtifactStagingDirectory) 81 | ArtifactName: pypi 82 | 83 | - job: BuildCliDiffToolPythonWheel 84 | displayName: 'Build Python Wheel for cli breaking change detection tool' 85 | 86 | condition: succeeded() 87 | pool: 88 | name: 'pool-ubuntu-2004' 89 | steps: 90 | - task: UsePythonVersion@0 91 | displayName: 'Use Python 3.12' 92 | inputs: 93 | versionSpec: 3.12 94 | 95 | - task: Bash@3 96 | displayName: 'Run Wheel Build Script' 97 | inputs: 98 | targetType: 'filePath' 99 | filePath: scripts/ci/build_cli_diff_tool.sh 100 | 101 | - task: PublishPipelineArtifact@0 102 | displayName: 'Publish Artifact: pypi' 103 | inputs: 104 | TargetPath: $(Build.ArtifactStagingDirectory) 105 | ArtifactName: pypi_cli_diff_tool 106 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | [build-system] 8 | # Minimum requirements for the build system to execute. 9 | requires = ["setuptools", "wheel"] 10 | -------------------------------------------------------------------------------- /scripts/ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | : "${BUILD_STAGINGDIRECTORY:?BUILD_STAGINGDIRECTORY environment variable not set}" 6 | : "${BUILD_SOURCESDIRECTORY:=$(cd $(dirname $0); cd ../../; pwd)}" 7 | 8 | cd "${BUILD_SOURCESDIRECTORY}" 9 | 10 | echo "Build azdev" 11 | pip install -U pip setuptools wheel 12 | python setup.py bdist_wheel -d "${BUILD_STAGINGDIRECTORY}" 13 | python setup.py sdist -d "${BUILD_STAGINGDIRECTORY}" 14 | -------------------------------------------------------------------------------- /scripts/ci/build_cli_diff_tool.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | : "${BUILD_STAGINGDIRECTORY:?BUILD_STAGINGDIRECTORY environment variable not set}" 6 | : "${BUILD_SOURCESDIRECTORY:=$(cd $(dirname $0); cd ../../; pwd)}" 7 | 8 | cd "${BUILD_SOURCESDIRECTORY}" 9 | cd ./azure-cli-diff-tool 10 | 11 | echo "Build azure cli diff tool" 12 | pip install -U pip setuptools wheel 13 | python setup.py bdist_wheel -d "${BUILD_STAGINGDIRECTORY}" 14 | python setup.py sdist -d "${BUILD_STAGINGDIRECTORY}" 15 | -------------------------------------------------------------------------------- /scripts/ci/extract_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Extract the version of the CLI from azdev's __init__.py file. 4 | : "${BUILD_STAGINGDIRECTORY:?BUILD_STAGINGDIRECTORY environment variable not set}" 5 | 6 | ver=`cat azdev/__init__.py | grep __VERSION__ | sed s/' '//g | sed s/'__VERSION__='// | sed s/\"//g` 7 | echo $ver > $BUILD_STAGINGDIRECTORY/version 8 | echo $ver > $BUILD_STAGINGDIRECTORY/azdev-${ver}.txt -------------------------------------------------------------------------------- /scripts/ci/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | echo "Install azdev into virtual environment" 6 | python -m venv env 7 | . env/bin/activate 8 | pip install -U pip setuptools wheel -q 9 | pip install $(find ${BUILD_ARTIFACTSTAGINGDIRECTORY}/pypi -name *.tar.gz) -q 10 | git clone https://github.com/Azure/azure-cli.git 11 | git clone https://github.com/Azure/azure-cli-extensions.git 12 | azdev setup -c -r azure-cli-extensions 13 | -------------------------------------------------------------------------------- /scripts/ci/run_tox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | pip install tox 6 | python -m tox 7 | -------------------------------------------------------------------------------- /scripts/license_verify.py: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See License.txt in the project root for 4 | # license information. 5 | # ----------------------------------------------------------------------------- 6 | 7 | # Verify that all *.py files have a license header in the file. 8 | 9 | import os 10 | import sys 11 | 12 | ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 13 | 14 | PY_LICENSE_HEADER = """# ----------------------------------------------------------------------------- 15 | # Copyright (c) Microsoft Corporation. All rights reserved. 16 | # Licensed under the MIT License. See License.txt in the project root for 17 | # license information. 18 | # ----------------------------------------------------------------------------- 19 | """ 20 | 21 | PY_LICENSE_HEADER_ALT = """# -------------------------------------------------------------------------------------------- 22 | # Copyright (c) Microsoft Corporation. All rights reserved. 23 | # Licensed under the MIT License. See License.txt in the project root for license information. 24 | # -------------------------------------------------------------------------------------------- 25 | """ 26 | 27 | env_folders = [ 28 | os.path.join(ROOT_DIR, 'env'), 29 | os.path.join(ROOT_DIR, 'env27'), 30 | os.path.join(ROOT_DIR, '.tox'), 31 | ] 32 | 33 | 34 | def contains_header(text): 35 | for header in [PY_LICENSE_HEADER, PY_LICENSE_HEADER_ALT]: 36 | if header in text: 37 | return True 38 | return False 39 | 40 | 41 | def get_files_without_header(): 42 | files_without_header = [] 43 | for current_dir, _, files in os.walk(ROOT_DIR): 44 | # skip folders generated by virtual env 45 | if any(d for d in env_folders if d in current_dir): 46 | continue 47 | 48 | for a_file in files: 49 | if a_file.endswith('.py'): 50 | cur_file_path = os.path.join(current_dir, a_file) 51 | with open(cur_file_path, 'r') as f: 52 | file_text = f.read() 53 | 54 | if len(file_text) > 0 and not contains_header(file_text): 55 | files_without_header.append((cur_file_path, file_text)) 56 | return files_without_header 57 | 58 | 59 | files_without_header = [file_path for file_path, file_contents in get_files_without_header()] 60 | 61 | if files_without_header: 62 | print("Error: The following files don't have the required license headers:", file=sys.stderr) 63 | print('\n'.join(files_without_header), file=sys.stderr) 64 | print("Error: {} file(s) found without license headers.".format(len(files_without_header)), file=sys.stderr) 65 | sys.exit(1) 66 | else: 67 | pass 68 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-cli-dev-tools/5130a24020ca0a47055e914a58a4af67d9eb98ad/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # ----------------------------------------------------------------------------- 4 | # Copyright (c) Microsoft Corporation. All rights reserved. 5 | # Licensed under the MIT License. See License.txt in the project root for 6 | # license information. 7 | # ----------------------------------------------------------------------------- 8 | 9 | """Azure Developer Tools package that can be installed using setuptools""" 10 | 11 | from codecs import open 12 | import os 13 | import re 14 | from setuptools import setup, find_packages 15 | 16 | 17 | azdev_path = os.path.dirname(os.path.realpath(__file__)) 18 | with open(os.path.join(azdev_path, 'azdev', '__init__.py'), 'r') as version_file: 19 | __VERSION__ = re.search(r'^__VERSION__\s*=\s*[\'"]([^\'"]*)[\'"]', 20 | version_file.read(), re.MULTILINE).group(1) 21 | 22 | with open('README.rst', 'r', encoding='utf-8') as f: 23 | README = f.read() 24 | with open('HISTORY.rst', 'r', encoding='utf-8') as f: 25 | HISTORY = f.read() 26 | 27 | setup( 28 | name='azdev', 29 | version=__VERSION__, 30 | description='Microsoft Azure CLI Developer Tools', 31 | long_description=README + '\n\n' + HISTORY, 32 | url='https://github.com/Azure/azure-cli-dev-tools', 33 | author='Microsoft Corporation', 34 | author_email='azpycli@microsoft.com', 35 | license='MIT', 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Intended Audience :: Developers', 39 | 'Topic :: Software Development :: Build Tools', 40 | 'Environment :: Console', 41 | 'License :: OSI Approved :: MIT License', 42 | 'Natural Language :: English', 43 | 'Programming Language :: Python :: 3.6', 44 | 'Programming Language :: Python :: 3.7', 45 | 'Programming Language :: Python :: 3.8', 46 | 'Programming Language :: Python :: 3.9', 47 | 'Programming Language :: Python :: 3.10' 48 | ], 49 | keywords='azure', 50 | python_requires='>=3.6', 51 | packages=[ 52 | 'azdev', 53 | 'azdev.config', 54 | 'azdev.operations', 55 | 'azdev.mod_templates', 56 | 'azdev.operations.help', 57 | 'azdev.operations.help.refdoc', 58 | 'azdev.operations.linter', 59 | 'azdev.operations.linter.rules', 60 | 'azdev.operations.linter.pylint_checkers', 61 | 'azdev.operations.testtool', 62 | 'azdev.operations.extensions', 63 | 'azdev.operations.statistics', 64 | 'azdev.operations.command_change', 65 | 'azdev.operations.breaking_change', 66 | 'azdev.operations.cmdcov', 67 | 'azdev.utilities', 68 | ], 69 | install_requires=[ 70 | 'azure-multiapi-storage', 71 | 'docutils', 72 | 'flake8', 73 | 'gitpython', 74 | 'jinja2', 75 | 'knack', 76 | 'pylint<4', 77 | 'pytest-xdist', # depends on pytest-forked 78 | 'pytest-forked', 79 | 'pytest>=5.0.0', 80 | 'pyyaml', 81 | 'requests', 82 | 'sphinx==1.6.7', 83 | 'tox', 84 | 'jsbeautifier~=1.14.7', 85 | 'deepdiff~=6.3.0', 86 | 'azure-cli-diff-tool~=0.1.0', 87 | 'packaging', 88 | 'tqdm', 89 | 'wheel==0.30.0', 90 | 'setuptools==70.0.0', 91 | 'microsoft-security-utilities-secret-masker~=1.0.0b4' 92 | ], 93 | package_data={ 94 | 'azdev.config': ['*.*', 'cli_pylintrc', 'ext_pylintrc'], 95 | 'azdev.mod_templates': ['*.*'], 96 | 'azdev.operations.linter.rules': ['ci_exclusions.yml'], 97 | 'azdev.operations.linter': ["data/*"], 98 | 'azdev.operations.cmdcov': ['*.*'], 99 | 'azdev.operations.breaking_change': ['*.*'], 100 | }, 101 | include_package_data=True, 102 | entry_points={ 103 | 'console_scripts': ['azdev=azdev.__main__:main'] 104 | } 105 | ) 106 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py36 4 | py37 5 | py38 6 | py39 7 | py310 8 | 9 | [testenv] 10 | whitelist_externals = 11 | pylint 12 | flake8 13 | commands= 14 | python ./scripts/license_verify.py 15 | python setup.py check -r -s 16 | pylint azdev --rcfile=.pylintrc -r n 17 | flake8 --statistics --append-config=.flake8 azdev 18 | --------------------------------------------------------------------------------