1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
|
The document provides instructions and guidelines on how to author individual commands.
> **Note**
>
> [aaz-dev-tools](https://azure.github.io/aaz-dev-tools/) is our codegen tool which can be used for Control Plane APIs. Highly recommend using it in new commands. It brings the following benefits:
> - Shorten development cycle (save 50%+ development time).
> - Consistent code style improves command quality and saves PR review time.
> - Directly generate CLI commands from swagger instead of waiting for SDK release.
>
> If you have any question, you can post it in this Teams [channel](https://aka.ms/azure-cli-codegen-channel).
**AUTHORING COMMANDS**
[1. Write the Command Loader](#write-the-command-loader)
[2. Write a Command](#write-a-command)
[3. Register Commands](#register-commands)
[4. Write Help Entry](#write-help-entry)
[5. Customize Arguments](#customize-arguments)
**ADDITIONAL TOPICS**
[6. Keyword Argument (kwarg) Reference](#keyword-argument-reference)
[7. Supporting the IDs Parameter](#supporting-the-ids-parameter)
[8. Supporting Name or ID Parameters](#supporting-name-or-id-parameters)
[9. Generic Update Commands](#generic-update-commands)
[10. Custom Table Formats](#custom-table-formats)
[11. Tab Completion (bash only)](#tab-completion)
[12. Validators](#validators)
[13. Registering Flag Arguments](#registering-flags)
[14. Registering Enum Arguments](#registering-enums)
[15. Preventing particular extensions from being loading](#extension-suppression)
[16. Deprecating Commands and Arguments](#deprecating-commands-and-arguments)
[17. Multi-API Aware Modules](#multi-api-aware-modules)
[18. Preview Commands and Arguments](#preview-commands-and-arguments)
Authoring Commands
=============================
## Write the Command Loader
As of version 2.0.24, Azure CLI is based on the Knack framework (https://github.com/Microsoft/knack), which uses the `CLICommandsLoader` class as the mechanism for loading a module. In Azure CLI, you will create your own loader which will inherit from the `AzCommandsLoader` class. The basic structure is:
```Python
class MyCommandsLoader(AzCommandsLoader):
def __init__(self, cli_ctx=None):
from azure.cli.core.sdk.util import CliCommandType
from azure.cli.core.profiles._shared import MGMT_MYTYPE
mymod_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.mymod.custom#{}')
super(MyCommandsLoader, self).__init__(cli_ctx=cli_ctx,
resource_type=MGMT_MYTYPE,
custom_command_type=mymod_custom)
def load_command_table(self, args):
# TODO: Register command groups and commands here
return self.command_table
def load_arguments(self, command):
# TODO: Register argument contexts and arguments here
COMMAND_LOADER_CLS = MyCommandsLoader
```
Note that `MGMT_MYTYPE` will need to be added to the `azure\cli\core\profiles\_shared.py` file. See [Multi-API Aware Modules](#multi-api-aware-modules)
```Python
class ResourceType(Enum): # pylint: disable=too-few-public-methods
...
MGMT_MYTYPE = ('azure.mgmt.mytype', 'MyTypeManagementClient')
...
```
## Write a Command
Write your command as a simple function, specifying your arguments as the parameter names.
***Parameter Naming Guidance***
When choosing names, it is recommended that you look at similar commands and follow those naming conventions to take advantage of any aliasing that may already be in place. For example, you should choose `resource_group_name` over `rg`, `resource_group` or some other shorthand, because this parameter is globally aliased and you will inherit the `-g` short option and the completer.
Avoid using a parameter name called `name` as this is a very common alias in the CLI and will often create aliasing conflicts.
If you specify a default value in your function signature, this will flag the argument as optional and will automatically display the default value in the help text for the command. Any parameters that do not have a default value are required and will automatically appear in help with the [Required] label. The required and default behaviors for arguments can be overridden (see Argument Customization below) but this is not generally needed.
***Special Arguments***
There are two arguments you may include in your custom command that are reserved by the infrastructure and have special meaning.
`cmd`: If used, this should be the first argument in your custom command, and allows you to access the command instance within your custom command. This will allow you to access the CLI context and numerous helper methods to make writing your command simpler, particularly when working with a multi-API style module.
`client`: If your command has registered the `client_factory` keyword argument, that factory will be passed into this variable. It can appear anywhere in your command signature.
## Register Commands
Before your command can be used in the CLI, it must be registered. Within the `load_command_table` method of your command loader, you will have something like:
```Python
# (1) Registering a command type for reuse among groups
mymod_sdk = CliCommandType(
operations_tmpl='azure.mgmt.mymod.operations#MyOperations.{}',
client_factory=cf_mymod
)
# (2) Registering a command group
with self.command_group('mymod', mymod_sdk) as g:
# (3) Registering different types of commands
g.command('command1', 'do_something_1')
g.custom_command('command2', 'do_something_2')
g.generic_update_command('update', custom_function_name='my_custom_update')
g.wait_command('wait')
g.show_command('show')
```
At this point, you should be able to access your command using `az [name]` and access the built-in help with `az [name] -h/--help`. Your command will automatically be 'wired up' with the global parameters. See below for amplifying information.
**(1) CliCommandType**
CliCommandType is a way to group and reuse and keyword arguments supported by commands. Earlier, in the `__init__` method of the `MyCommandsLoader` class, we created a `mymod_custom` variable and assigned it to the `custom_command_type` keyword argument. This will be used any time you use the `custom_command` method within a command group. It is registered with the loader since most modules typically put all of their custom methods in a single file.
**(2) Command Group Helper**
```Python
command_group(self, group_name, command_type=None, **kwargs)
```
- `group_name`: the group name ('network', 'storage account', etc.)
- `command_type`: a `CliCommandType` object that will be used for all calls to `command` within the group.
- `kwargs`: any supported kwarg that will be used as the basis for all command calls. Commonly used kwargs include: `custom_command_type` (if custom commands are split amongst many files) and `client_factory` (if custom commands use the `client` argument).
**(3) Command Registration Helpers**
***command***
```Python
command(self, name, method_name=None, command_type=None, **kwargs)
```
- `name`: The name of the command within the command group
- `method_name`: The name of the SDK or custom method, relative to the path specified in `operations_tmpl`.
- `command_type`: A `CliCommandType` object to apply to this command (optional).
- `kwargs`: any supported kwarg. Commonly used kwargs include `validator`, `table_transformer`, `confirmation`, `supports_no_wait` and `transform`.
Any kwargs that are not specified will be pulled from the `command_type` kwarg, if present.
***custom_command***
The signature for `custom_command` is exactly the same as `command`. The only difference is that, whereas `command` uses `command_type` as the fallback for missing kwargs, `custom_command` relies on `custom_command_type`.
***generic_update_command***
See the section on [Generic Update Commands](#generic-update-commands)
***wait_command***
The generic wait command provides a template solution for polling Azure resources until specific conditions are met.
```Python
wait_command(self, name, getter_name='get', **kwargs)
```
- `name`: The name of the command within the command group. Commonly called 'wait'.
- `getter_name`: The name of the method for the object getter, relative to the path specified in `operations_tmpl`.
- `kwargs`: any supported kwarg.
Since most wait commands rely on a simple GET call from the SDK, most of these entries simply look like:
```Python
g.wait_command('wait')
```
***custom_wait_command***
Similar to `custom_command` and `command`, the signature for `custom_wait_command` is exactly the same as `wait_command` but uses `custom_command_type` as the fallback for missing kwargs.
***show_command***
The generic show command ensures a consistent behavior when encountering a missing Azure resource.
With little exception, all `show` commands should be registered using this method or `custom_show_command` to ensure consistency.
```Python
show_command(self, name, getter_name='get', **kwargs)
```
- `name`: The name of the command within the command group. Commonly called 'show'.
- `getter_name`: The name of the method for the object getter, relative to the path specified in `operations_tmpl`.
- `kwargs`: any supported kwarg.
***custom_show_command***
Similar to `custom_command` and `command`, the signature for `custom_show_command` is exactly the same as `show_command` but uses `custom_command_type` as the fallback for missing kwargs.
**(4) Supporting --no-wait**
When registering a command, the boolean `supports_no_wait` property can be used to specify that the command supports `--no-wait`.
Here are examples:
***command()***
```Python
with self.command_group('mymod', mymod_sdk) as g:
g.command('command1', 'do_something_1', supports_no_wait=True)
```
***custom_command()***
```Python
# inside load_command_table(...)
with self.command_group('mymod', mymod_sdk) as g:
g.custom_command('command2', 'do_something_2', supports_no_wait=True)
# inside custom.py
from azure.cli.core.util import sdk_no_wait
def do_something_2(cmd, arg1, arg2, no_wait=False):
return sdk_no_wait(no_wait, client.create_or_update, arg1, arg2)
```
The signature of `azure.cli.core.util.sdk_no_wait` is:
```Python
sdk_no_wait(no_wait, func, *args, **kwargs)
```
- `no_wait` - The boolean for no wait. `True` if `--no-wait` specified. `False` otherwise.
- `func` - The callable to use.
- `args` - The positional arguments that should be passed to the callable.
- `kwargs` - The keyword arguments that should be passed to the callable.
***generic_update_command()***
```Python
with self.command_group('mymod', mymod_sdk) as g:
g.generic_update_command('update', supports_no_wait=True)
```
**(5) Supporting --defer**
When registering a command, the boolean `supports_local_cache` property can be used to specify that the command supports `--defer`. This will allow traditional GET and PUT requests to interact with the CLI's local object cache instead of making
calls on the wire either for performance reasons (to avoid network latency) or because the service will only accept a payload constructed from many calls.
See [Commands With Complex Types](https://github.com/Azure/azure-cli/blob/dev/doc/command_guidelines.md#commands-with-complex-types)
Here are examples:
***custom_command()***
```Python
# inside load_command_table(...)
with self.command_group('mymod', mymod_sdk) as g:
g.custom_command('command2', 'do_something_2', supports_local_cache=True)
# inside custom.py
def do_something_2(cmd, client, arg1, arg2, no_wait=False):
from azure.cli.core.commands import cached_get, cached_put
item = cached_get(cmd, client.get, arg1, arg2)
# TODO: perform some mutation of item
return cached_put(cmd, client.create_or_update, arg1, arg2, item)
```
Cached objects are deleted upon a successful PUT and can be view and managed using the `az cache` commands.
## Write Help Entry
See the following for guidance on writing a help entry: https://github.com/Azure/azure-cli/blob/master/doc/authoring_help.md
## Customize Arguments
While the CLI will attempt to figure out certain key properties of your command and its arguments, it is often necessary to override, add to, or customize this metadata. To modify/enhance your command arguments, create an argument context. For the standard modules, these entries are contained within a file called `_params.py`. Within the `load_arguments` method of your command loader, you will have something like:
```Python
# (1) Create an argument context
with self.argument_context('mymod') as c:
# (2) Register different kinds of arguments
c.argument('name', options_list=['--name', '-n'], help='Name of the thing.', completer=get_resource_name_completion_list('Microsoft.Example/mything'))
c.extra('extra_thing', options_list=['--extra'], help='An extra thing.')
c.ignore('ignore_this', 'ignore_that')
c.argument('some_flag', arg_type=get_three_state_flag())
c.argument('some_enum', arg_type=get_enum_type(MyEnum, default='foo'))
```
For more information:
**(1) Create an argument context**
```Python
argument_context(self, scope, **kwargs):
```
- `scope` - This string is the level at which your customizations are applied. For example, consider the case where you have commands `az mypackage command1` and `az mypackage command2`, which both have a parameter `my_param`.
```Python
with self.argument_context('mypackage', ...) as c: # applies to BOTH command1 and command2
```
But
```Python
with self.argument_context('mypackage command1', ...) as c: # applies to command1 but not command2
```
Like CSS rules, modifications are applied in order from generic to specific.
```Python
with self.argument_context('mypackage', ...) as c: # applies to both command1 and command2
c.argument('my_param', ...)
with self.argument_context('mypackage command1', ...) as c: # applies to command1 but not command2
c.argument('my_param', ...)
```
- `kwargs` - Any supported kwarg which will be applied to calls within the context block.
**(2) Register different kinds of arguments**
***argument***
```Python
argument(self, dest, arg_type=None, **kwargs)
```
- `dest` - The name of the parameter that you are targeting.
- `arg_type` - An instance of the `azure.cli.core.commands.CliArgumentType` class. This essentially serves as a named, reusable packaging of the `kwargs` that modify your command's argument. It is useful when you want to reuse an argument definition, but is generally not required. It is most commonly used for name type parameters, or for enums and flags.
- `kwargs` - Most likely, you will simply specify keyword arguments that will accomplish what you need. Any `kwargs` specified will override or extended the definition in `arg_type`, if provided.
***ignore***
```Python
ignore(self, *args)
```
- `args` - one or more parameter names (dest values) that should be ignored. Useful to suppress arguments that appear due to reflection on an SDK, but are unwanted in the command signature.
***extra***
```Python
extra(self, dest, arg_type=None, **kwargs)
```
Arguments are the same as `argument`, however this will create a new parameter whereas `argument` will not. This is useful when a reflected SDK method is missing a parameter that you need to expose in your command.
Additional Topics
=============================
## Keyword Argument Reference
**Overview of Keyword Arguments in the Azure CLI**
When writing commands for the Azure CLI, it is important to understand how keyword arguments (kwargs) are applied. Refer to the following diagram.

From the diagram you can see that any kwargs supplied when creating the `AzCommandsLoader` object are passed to and used as the baseline for any command groups or argument contexts that are later created. Any kwargs specified in the `command_group` calls serve as the baseline for any `command` or `custom_command` calls, and any kwargs passed to `argument_context` serve as the baseline for any calls to `argument`.
While kwargs are inherited from higher levels on the diagram, they can be overridden at a lower level. For example, if `custom_command_type=foo` is used as a module-level kwarg in the `AzCommandLoader.__init__` method and `custom_command_type=bar` is passed for a call to `command_group`, then `bar` will be used for all calls to `custom_command` within that command group.
Additionally, you can see that kwargs registered on a command group *do not* carry over to argument contexts, so you must apply the kwargs in both places if necessary.
****Command Group****
_Special Kwargs_
The following special kwargs are supported by command group and its helper methods:
- `table_transformer` - See section on [Custom Table Formats](#custom-table-formats)
- `validator` - See section on [Validators](#validators)
- `confirmation` - During interactive use, will prompt the user to confirm their choice to proceed. Supply a value of True to use the default prompt, or supply a string to use a custom prompt message. If the command is invoked in non-interactive scenarios and the --yes/-y parameter is not supplied, the command will fail.
- `transform` - Accepts a callable that takes a command result, which can be manipulated as desired. The transformed result is then returned. In general, output formats should closely mirror those returned by the service, and so this should be infrequently used. The modifies the output *regardless of the output format type*.
- `deprecate_info` - See [Deprecating Commands and Arguments](https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#deprecating-commands-and-arguments)
- `formatter_class` - Advanced. Accepts a custom class that derives from `argparse.HelpFormatter` to modify the help document generation.
- `argument_loader` - Advanced. Accepts a callable that takes no parameters which will be used in place of the default argument loader.
- `description_loader` - Advanced. Accepts a callable that takes no parameters which will be used in place of the default description loader.
- `is_preview` - See [Preview Commands and Arguments](https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#preview-commands-and-arguments)
_General Kwargs_
The following kwargs may be inherited from the command loader:
- `min_api` - Minimum API version for which the command will appear.
- `max_api` - Maximum API version for which the command will appear.
- `resource_type` - An `azure.cli.core.profiles.ResourceType` enum value that is used for multi-API packages.
- `operation_group` - Only used by the `azure-cli-vm` module to specify which resource API to target.
- `command_group` - A `CliCommandType` object that contains a bundle of kwargs that will be used by the `command` method if not otherwise provided.
- `custom_command_group` - A `CliCommandType` object that contains a bundle of kwargs that will be used by the `custom_command` method if not otherwise provided.
****Argument Context****
_Special Kwargs_
The follow special kwargs are supported by argument context and its helper methods:
- `options_list` - By default, your argument will be exposed as an option in hyphenated form (ex: `my_param` becomes `--my-param`). If you would like to change the option string without changing the parameter name, and/or add a short option, specify the `options_list` kwarg. This is a list of string values. If there will only be one value, you can just specify the value (Ex: `options_list=['--myparam', '-m']` or `options_list='--myparam'`)
- `validator` - See section on [Validators](#validators)
- `completer` - See section on [Tab Completion](#tab-completion)
- `id_part` - See section on [Supporting the IDs Parameter](#supporting-the-ids-parameter).
- `arg_group` - Groups arguments within this context under a group name or add an argument to the group. This group name is shown in the help for the command. For example if `arg_group` is "Network", all applicable arguments will be grouped under the heading "Network Arguments" in the help text for the command.
- `is_preview` - See [Preview Commands and Arguments](https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#preview-commands-and-arguments)
Additionally, the following `kwargs`, supported by argparse, are supported as well:
- `nargs` - See https://docs.python.org/3/library/argparse.html#nargs
- `action` - See https://docs.python.org/3/library/argparse.html#action
- `const` - See https://docs.python.org/3/library/argparse.html#const
- `default` - See https://docs.python.org/3/library/argparse.html#default. Note that the default value is inferred from the parameter's default value in the function signature. If specified, this will override that value.
- `type` - See https://docs.python.org/3/library/argparse.html#type
- `choices` - See https://docs.python.org/3/library/argparse.html#choices. If specified this will also serve as a value completer for people using tab completion. However, it is not recommended that you use this because the choice lists will be case sensitive. Instead see "Registering Enums".
- `required` - See https://docs.python.org/3/library/argparse.html#required. Note that this value is inferred from the function signature depending on whether or not the parameter has a default value. If specified, this will override that value.
- `help` - See https://docs.python.org/3/library/argparse.html#help. Generally you should avoid adding help text in this way, instead opting to create a help file as described above.
- `metavar` - See https://docs.python.org/3/library/argparse.html#metavar
_General Kwargs_
The following kwargs may be inherited from the command loader:
- `min_api` - Minimum API version for which the argument will appear. Otherwise the argument will be ignored.
- `max_api` - Maximum API version for which the argument will appear. Otherwise the argument will be ignored.
- `resource_type` - An `azure.cli.core.profiles.ResourceType` enum value that is used for multi-API packages.
- `operation_group` - Only used by the `azure-cli-vm` module to specify which resource API to target.
## Supporting the IDs Parameter
Most ARM resources can be identified by an ID. In many cases, for example `show` and `delete` commands, it may be more useful to copy and paste an ID to identify the target resource instead of having to specify the names of the resource group, the resource, and the parent resource (if any).
Azure CLI supports exposing an `--ids` parameter that will parse a resource ID into its constituent named parts so that this parsing need not be done as part of a client script. Additionally `--ids` will accept a _list_ of space-separated IDs, allowing the client to loop the command over each ID.
Enabling this functionality only requires the command author specify the appropriate values for `id_part` in their calls to `AzArgumentContext.argument`.
Consider the following simplified example for NIC IP config.
```Python
def show_nic_ip_config(resource_group_name, nic_name, ip_config_name):
# retrieve and return the IP config
# inside load_command_table(...)
with self.command_group('network nic ip-config', network_nic_sdk) as g:
g.custom_command('show', 'show_nic_ip_config')
# inside load_arguments(...)
with self.argument_context('network nic ip-config') as c:
c.argument('nic_name', help='The NIC name.')
c.argument('ip_config_name', options_list=['--name', '-n'], help='The IP config name.')
```
The help output for this command would be:
```
Arguments
--name -n : The IP config name.
--nic-name : The NIC name.
--resource-group -g: Name of resource group.
```
Now let's specify values for the `id_part` kwarg in the calls to `argument`:
```Python
def show_nic_ip_config(resource_group_name, nic_name, ip_config_name):
# retrieve and return the IP config
# inside load_command_table(...)
with self.command_group('network nic ip-config', network_nic_sdk) as g:
g.custom_command('show', 'show_nic_ip_config')
# inside load_arguments(...)
with self.argument_context('network nic ip-config') as c:
c.argument('nic_name', help='The NIC name.', id_part='name')
c.argument('ip_config_name', id_part='child_name_1', options_list=['--name', '-n'], help='The IP config name.')
```
The help output becomes:
```
Arguments
Resource Id Arguments
--ids : One or more resource IDs. If provided, no other 'Resource Id' arguments
should be specified.
--name -n : The IP config name.
--nic-name : The NIC name.
--resource-group -g: Name of resource group.
```
Now the user may identify the target IP config by specifying either the resource group, NIC and IP config names or by simply pasting in the ID for the IP config itself.
This feature is powered by the `parse_resource_id` helper method within the `msrestazure` package, which parses a resource ID into a dictionary. Specifying `id_part` maps the parsed value for a given key in that dictionary into your argument.
For example, consider the following ID of a subnet lock:
```
subscription/0000-0000-0000-0000/resourceGroups/myresourcegroup/Microsoft.Network/virtualNetworks/myvnet/subnets/mysubnet /providers/Microsoft.Authorization/locks/mylock
```
When run through `parse_resource_id`, the following dictionary results:
```Python
{
"subscription": "0000-0000-0000-0000",
"resource_group": "myresourcegroup",
"namespace": "Microsoft.Network",
"resource_type": "virtualNetworks",
"name": "myvnet",
"child_type_1": "subnets",
"child_name_1": "mysubnet",
"child_namespace_2": "Microsoft.Authorization",
"child_type_2": "locks",
"child_name_2": "mylock"
}
```
Any of these keys could be supplied as a value for `id_part`, thought typically you would only use `name`, `child_name_1`, `child_name_2`, etc.
A couple things to note:
- Currently, `--ids` is not exposed for any command that is called 'create', even if it is configured properly.
- `--ids` is intended to be the ID of the resource the command group is about. Thus, it needs to be suppressed on `list` commands for child resources. This simplest way to do this:
```Python
with self.argument_context('parent child') as c:
c.argument('parent_name', id_part=None) # This should ALWAYS be the id_part that was 'name'.
c.argument('child_name', ...)
```
## Supporting Name or ID Parameters
Often times, the service needs references to supporting resources like storage accounts, key vault, etc. Typically, services require the ARM ID of these resources. The CLI pattern is to accept the ARM ID for this resource OR the name of the resource, assuming the resource is in the same subscription and resource group as the main resource.
DO NOT:
- Expose an ID parameter like `--storage-account-id`.
- Add parameters like `--storage-account-resource-group` to indicate the resource group for the secondary resource. The user should supply the ARM ID in this instance.
DO:
- Call the parameter `--storage-account` and indicate in the help text that it accepts the "Name or ID of the storage account."
- Add logic similar to the following to a validator or custom command to process the name or ID logic:
**Custom Command**
```Python
def my_command(cmd, resource_group_name, foo_name, storage_account):
from azure.cli.core.commands.client_factory import get_subscription_id
from msrestazure.tools import is_valid_resource_id, resource_id
if not is_valid_resource_id(storage_account):
storage_account = resource_id(
subscription=get_subscription_id(cmd.cli_ctx),
resource_group=resource_group_name,
namespace='Microsoft.Storage', type='storageAccounts',
name=storage_account
)
```
**Validator**
```Python
def validate_storage_name_or_id(cmd, namespace):
from azure.cli.core.commands.client_factory import get_subscription_id
from msrestazure.tools import is_valid_resource_id, resource_id
if namespace.storage_account:
if not is_valid_resource_id(namespace.storage_account):
namespace.storage_account = resource_id(
subscription=get_subscription_id(cmd.cli_ctx),
resource_group=namespace.resource_group_name,
namespace='Microsoft.Storage', type='storageAccounts',
name=namespace.storage_account
)
```
## Generic Update Commands
The update commands within the CLI expose a set of generic update arguments: `--add`, `--remove` and `--set`. This allows the user to manipulate objects in a consistent way that may not have long option flags supported by the command. The method which exposes these arguments is `AzCommandGroup.generic_update_command` in the `azure.cli.core.commands` package. The signature of this method is:
```Python
generic_update_command(self, name,
getter_name='get', getter_type=None,
setter_name='create_or_update', setter_type=None, setter_arg_name='parameters',
child_collection_prop_name=None, child_collection_key='name', child_arg_name='item_name',
custom_func_name=None, custom_func_type=None, **kwargs)
```
Since most generic update commands can be reflected from the SDK, the simplest form this command can take is:
```Python
with self.command_group('test', test_sdk) as g:
g.generic_update_command('update')
```
However, most commonly, the `custom_func_name` and `custom_func_type` kwargs will be used to expose convenience arguments in addition to the generic arguments.
- `name` - The name of the command. Most commonly 'update'.
- `getter_name` - The name of the method which will be used to retrieve the object instance. If the method is named `get` (which is the case for most SDKs), this can be omitted.
- `getter_type` - A `CliCommandType` object which will be used to locate the getter. Only needed if the getter is a custom command (uncommon).
- `setter_name` - The name of the method which will be used to update the object instance using a PUT method. If the method is named `create_or_update` (which is the case for most SDKs), this can be omitted.
- `setter_type` - A `CliCommandType` object which will be used to locate the setter. Only needed if the setter is a custom command (uncommon).
- `setter_arg_name` - The name of the argument in the setter which corresponds to the object being updated. If the name is `parameters` (which is the case for most SDKs), this can be omitted.
- `custom_func_name` (optional) - The name of a method which accepts the object being updated (must be named `instance`), mutates, and returns that object. This is commonly used to add convenience options to the command by listing them in the method signature, similar to a purely custom method. The difference is that a custom command function returns the command result while a generic update custom function returns only the object being updated. A simple custom function might look like:
```Python
def my_custom_function(instance, item_name, custom_arg=None):
if custom_arg:
instance.property = custom_arg
return instance
```
In this case the `custom_func_name` would be `my_custom_function`.
- `custom_func_type` - A `CliCommandType` object which will be used to locate the custom function. If omitted, the CLI will look for and attempt to use the `custom_command_type` kwarg.
- `kwargs` - Any valid command kwarg.
**Working With Child Collections and Properties (Advanced)**
Sometimes you will want to write commands that operate on child resources and it may be that these child resources don't have dedicated getters and setters. In these cases, you must rely on the getter and setter of the parent resource. For example, consider an object `my_parent` which has a child collection `my_child` which in turn has its own child collection `my_grandchild`. The key property for all of these objects is simply `name`. For these cases, `generic_update_command` has three additional parameters:
- `child_collection_prop_name` - the name path to the child collection property, using dot syntax. To access `my_child`, the value would be `my_child`. To access `my_grandchild`, the value would be `my_child.my_grandchild`.
- `child_collection_key` - Most child collections in Azure are lists of objects (as opposed to dictionaries) which will have a property in them that serves as the key. This is the name of that key property. By default it is `name`. To refer to `my_child`, the value would be `name`. To refer to `my_grandchild` the value would be `name.name`.
- `child_arg_name` - If you want to refer the child object key (the property identified by `child_collection_key`) inside a custom function, you should specify the argument name you use in your custom function. By default, this is called `item_name`. In the above example, where our child object had a key called `name`, you could refer to this property within your custom function through the `item_name` property, or specify something different. For grandchild collections, use dot syntax (i.e.: `child_name.grandchild_name`).
**Logic Flow**
A simplified understanding of the flow of the generic update is as follows:
```Python
instance = getter(...) # retrieve the object
if custom_function:
instance = custom_function(...) # apply custom logic
instance = _process_generic_updates(...) # apply generic updates, which will overwrite custom logic in the event of a conflict
return setter(instance) # update the instance and return the result
```
**Generic Update for PATCH-based Services**
`generic_update_command` was designed to simulate PATCH-like behavior for services that are backed only by a PUT API endpoint. For services that have actual PATCH-based update endpoints, the CLI's `update` command should still leverage `generic_update_command` in order to provide consistency among commands. The following guidelines should be helpful:
- You'll probably need to specify the `setter_name` since it will likely be `update` instead of `create_or_update` (the default).
- You will HAVE TO supply `custom_func_name` and `custom_func_type`. Consider the following example:
```Python
def my_custom_foo_update(instance, prop1=None, prop2=None, complex_prop1=None, complex_prop2=None):
from my_foo_sdk import FooUpdateParameters, ComplexProperty
# (1) instantiate the update parameters object. Generally, you can pass simple parameters
# as-is and the service will correctly interpret this.
parameters = FooUpdateParameters(
prop1=prop1,
prop2=prop2)
# (2) complex objects must also have PATCH-like behavior, and often services do not
# correctly support this. You may need to fill these objects with the existing
# values if they are not being updated
parameters.complex_prop = ComplexProperty(
complex_prop1=complex_prop1 or instance.complex_prop.complex_prop1,
complex_prop2=complex_prop2 or instance.complex_prop.complex_prop2
)
# (3) instead of returning the instance object as you do with a PUT-based generic update,
# return the update parameters object.
return parameters
```
## Custom Table Formats
By default, when the `-o/--output table` option is supplied, the CLI will display the top level fields of the object structure as the columns of the table. The user can always specify a `--query` to control table and TSV formats, but the CLI also allows the command author to specify a different default table format. Two options exist:
**Supply a Callable**
Supply a callable that accepts the result as input and returns a list of OrderedDicts:
```Python
def transform_foo(result):
result = OrderedDict([('name', result['name']),
('resourceGroup', result['resourceGroup']),
('location', result['location'])])
return result
```
**Supply a JMESPath String**
A string containing Python dictionary-syntax '{Key:JMESPath path to property, ...}'
Example:
```Python
table_transformer='{Name:name, ResourceGroup:resourceGroup, Location:location, ProvisioningState:provisioningState, PowerState:instanceView.statuses[1].displayStatus}'
```
## Tab Completion
Tab completion is enabled by default (in bash or `az interactive`) for command names, argument names and argument choice lists. To enable tab completion for dynamic properties (for example, resource names) you can supply a callable which returns a list of options:
**get_resource_name_completion_list(resource_type)**
Since many completers simply return a list of resource names, you can use the `get_resource_name_completion_list` method from `azure.cli.core.commands.parameters` which accepts the type of resource you wish to get completions for.
Example:
```Python
completer=get_resource_name_completion_list('Microsoft.Compute/virtualMachines')
```
The behavior of the completer will depend on what the user has entered prior to hitting [TAB][TAB]. For example:
`az vm show -n [TAB][TAB]`
This will show VM names within the entire subscription.
`az vm show -g myrg -n [TAB][TAB]`
This will show VM names only within the provided resource group.
**Custom Completer**
```Python
from azure.cli.core.decorators import Completer
@Completer
def get_foo_completion_list(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument
# TODO: Your custom logic here
result = ...
return [r.name for r in result]
```
The method signature must be as shown and it must return a list of names. You also must use the `@Completer` decorator. You can make additional REST calls within the completer and may examine the partially processed namespace via `namespace` to filter the list based on already supplied args (as in the above case with VM).
## Validators
Validators are custom pieces of code that allow you to perform any custom logic or validation on the entire namespace prior to command execution. Their structure is as follows:
```Python
def validate_my_arg(namespace):
if namespace.foo:
# TODO: Your custom logic here
...
```
If you need access to the `cli_ctx` within your validator (for instance, to make additional REST calls) the following signature can be used:
```Python
def validate_my_arg(cmd, namespace):
if namespace.foo:
# TODO: Your custom logic here
client=my_client_class(cmd.cli_ctx)
...
```
Validators are executed after argument parsing, and thus after the native argparse-supported `type` and `action` have been applied. A CLIError raised during a validator execution will be treated as a validation error, which will result in the display of the command usage string, as opposed to the same error raised within a custom command, which will just print the error and no validation string.
***Command Validators***
The `validator` keyword applies to commands and arguments. A command can have, at most, one validator. If supplied, then *only* this validator will be executed. Any argument-level validators will be ignored. The reason to use a command validator is if the validation sequence is important. However, the command validator can and very often is composed to individual argument level validators. You simply define the sequence in which they execute.
***Argument Validators***
An argument can be assigned, at most, one validator. However, since a command can have many arguments, this means that a command can have many argument validators. Furthermore, since an argument context may apply to many commands, this means that this argument validator can be reused across many commands. At execution time, argument validators are executed *in random order*, so you should ensure you do not have dependencies between validators. If you do, the a command validator is the appropriate route to take. It is fine to have an argument validator involve several parameters as long as they are interdependent (for example, a validator involving a vnet name and subnet name).
## Registering Flags
The recommended way to register boolean properties in the CLI is using the `get_three_state_flag` arg_type.
```Python
with self.argument_context('mymod') as c:
c.argument('bool_prop_enabled', arg_type=get_three_state_flag())
```
This will allow the user to specify the option as a flag or as a true/false parameter and allows for maximum convenience and consistency between create and update commands. With the above registration, the following inputs would be accepted:
```
az mymod --bool-prop-enabled # bool_prop_enabled = TRUE
az mymod --bool-prop-enabled true # bool_prop_enabled = TRUE
az mymod --bool-prop-enabled false # bool_prop_enabled = FALSE
```
For flags that are switches on the command itself and not persisted as properties of a resource, the flag should be registered as follows:
```Python
with self.argument_context('mymod') as c:
c.argument('command_switch', action='store_true')
```
In this case, it will only accept the flag form. Do not use this for boolean properties.
## Registering Enums
The recommended way to register enums (either reflected from an SDK or custom choice lists) is to use the `get_enum_type` arg_type.
```Python
from azure.mgmt.mymod.models import MyModelEnum
with self.argument_context('mymod') as c:
c.argument('my_enum', arg_type=get_enum_type(MyModelEnum))
c.argument('my_enum2', arg_type=get_enum_type(['choice1', 'choice2']))
```
Above are two examples of how this can be used. In the first instance, an Enum model is reflected from the SDK. In the second instance, a custom choice list is provided. This is preferable to using the native `argparse.choices` kwarg because the choice lists generated by `get_enum_type` will be case insensitive.
## Extension Suppression
It is possible for a command module to suppress specific extensions from being loaded.
This is useful for commands that were once extensions that have now moved inside a command module.
Here, we suppress an extension by name and also by version.
This will allow the extension to be published in the future with the same name and a newer version that will not be suppressed.
This is great for experimental extensions that periodically get incorporated into the product.
```Python
class MyCommandsLoader(AzCommandsLoader):
def __init__(self, cli_ctx=None):
from azure.cli.core import ModExtensionSuppress
# Suppress myextension up to and including version 0.2.0
super(MyCommandsLoader, self).__init__(cli_ctx=cli_ctx,
suppress_extension=ModExtensionSuppress(__name__, 'myextension', '0.2.0',
reason='These commands are now in the CLI.',
recommend_remove=True))
```
## Deprecating Commands and Arguments
The CLI has built-in support for deprecating the following: commands, command groups, arguments, option values. Deprecated items will appear with a warning in the help system or when invoked. The following keyword arguments are supported when deprecating an item:
- `target`: The thing being deprecated. This is often not needed as in most cases the CLI can figure out what is being deprecated.
- `redirect`: This is the alternative that should be used in lieu of the deprecated thing. If not provided, the item is expected to be removed in the future with no replacement.
- `hide`: Hide the deprecated item from the help system, reducing discoverability, but still allow it to be used. Accepts either the boolean `True` to immediately hide the item or a core CLI version. If a version is supplied, the item will appear until the core CLI rolls to the specified value, after which it will be hidden.
- `expiration`: Accepts a core CLI version at which the deprecated item will no longer function. This version will be communicated in all warning messages.
Deprecation of different command elements are usually accomplished using the `deprecate_info` kwarg in conjunction with a `deprecate` helper method.
***Deprecate Command Group***
```Python
with self.command_group('test', test_sdk, deprecate_info=self.deprecate(redirect='new-test', hide=True)) as g:
g.show_command('show', 'get')
g.command('list', 'list')
```
This will deprecate the entire command group `test`. Note that call to `self.deprecate`, calling the deprecate helper method off of the command loader. The warning message for this would read: ```This command group has been deprecated and will be removed in a future release. Use `new-test` instead.```
Additionally, since the command group is deprecated then, by extension, all of the commands within it are deprecated as well. They will not be marked as such, but will display a warning:
```This command has been implicitly deprecated because command group `test` is deprecated and will be removed in a future release. Use `new-test` instead.```
***Deprecate Command***
```Python
with self.command_group('test', test_sdk) as g:
g.command('show-parameters', 'get_params', deprecate_info=g.deprecate(redirect='test show', expiration='2.1.0'))
```
This will deprecate the command `test show-parameters`. Note that call to `g.deprecate`, calling the deprecate helper method off of the command group. The warning message for this would read: ```This command has been deprecated and will be removed in version 2.1.0. Use `test show` instead.```
***Deprecate Argument***
```Python
with self.argument_context('test show-parameters') as c:
c.argument('junk_flag', help='Something we no longer want to support.' deprecate_info=c.deprecate(expiration='2.1.0'))
```
This will deprecate the argument `--junk-flag` on `test show-parameters`. Note that call to `c.deprecate`, calling the deprecate helper method off of the argument context. The warning message for this would read: ```Argument `--junk-flag` has been deprecated and will be removed in version 2.1.0.```
***Deprecate Argument Option***
```Python
with self.argument_context('test show-parameters') as c:
c.argument('resource', options_list=['--resource', c.deprecate(target='--resource-id', redirect='--target')])
```
This will deprecate the argument `--resource-id` option on `test show-parameters` in favor of `--resource`. Note that call to `c.deprecate`, calling the deprecate helper method off of the argument context. The warning message for this would read: ```Option `--resource-id` has been deprecated and will be removed in a future release. Use `--resource` instead.``` Here you must specify `target` in order to identify the deprecated option. When an option value is deprecated, it appears in help as two separate arguments, with the deprecation warning on the deprecated option.
## Multi-API Aware Modules
To convert a module that used a mono-versioned SDK to one that works with multiple API versions:
1. In `azure.cli.core.profiles._shared.py` register your SDK and client in the `ResourceType` enum:
```Python
class ResourceType(Enum):
MGMT_MYSERVICE = ('azure.mgmt.myservice, MyServiceManagementClient') # REGISTER YOUR SDK
...
```
2. In the `AZURE_API_PROFILES` dictionary in that same file, for each profile your service applies to, add an entry for it like this:
```Python
AZURE_API_PROFILES = {
'latest': {
ResourceType.MGMT_MYSERVICE: '2019-03-01' # the supported API version on that profile
...
},
'2020-09-01-hybrid': {
ResourceType.MGMT_MYSERVICE: '2018-08-01' # different API version for this profile
...
},
...
}
```
3. Update imports in your files. They must use the API profile-aware "get_models" method and have access to a command or CLI object.
Example:
```Python
from azure.mgmt.myservice import Foo, Boo
def my_command(...):
# do stuff
```
Converted:
```Python
def my_command(cmd, ...):
Foo, Boo = cmd.get_models('Foo', 'Boo')
# do stuff
```
4. Use appropriate conditionals to ensure your command can run on all supported profiles:
***commands.py***
```Python
with self.command_group('test') as g:
g.command('use-new-feature', 'use_new_feature', min_api='2018-03-01') # won't be available unless min API is met
```
***params.py***
```Python
with self.argument_context('test create') as c:
c.argument('enable_new_feature', min_api='2018-03-01', arg_type=get_three_state_flag()) # expose argument only when min API is satisfied
```
***custom.py***
```Python
def my_test_command(cmd, ...):
Foo = cmd.get_models('Foo')
my_foo = Foo(...)
# will still work with older API versions because this branch will be skipped
if cmd.supported_api_version(min_api='2018-03-01'):
my_foo.enable_new_feature = enable_new_feature
return client.create_or_update(..., my_foo)
```
See earlier topics for other kwargs that can be used with multi-API idioms.
## Preview Commands and Arguments
The CLI has built-in support for marking commands, command groups and arguments as being in "preview" status. Preview items will appear with a warning in the help system or when invoked. Items marked preview can be changed, broken or removed at any time without following the deprecation process.
**Note that ANYTHING not marked "preview" is considered GA and thus a breaking change can only be enacted by following the deprecation mechanism (see earlier topic).**
Items are marked Preview using the `is_preview=True` kwarg. See the following for examples:
***Preview Command Group***
```Python
with self.command_group('test', test_sdk, is_preview=True) as g:
g.show_command('show', 'get')
...
```
Additionally, since the command group is in preview then, by extension, all of the commands and arguments within it are in preview as well. No message will be displayed for implicitly in-preview arguments, but a warning will be displayed for implicitly in-preview commands.
***Preview Command***
```Python
with self.command_group('test', test_sdk) as g:
g.command('show-parameters', 'get_params', is_preview=True)
```
This will declare just the command `test show-parameters` as being in preview. This command will appear with the `[Preview]` status tag when viewed in group help whereas other commands in the `test` group will not, indicating that only this command (and, by implication, its arguments) are in preview status.
***Preview Argument***
```Python
with self.argument_context('test show-parameters') as c:
c.argument('cool_flag', help='Something cool new flag.', is_preview=True)
```
This will mark the argument `--cool-flag` on `test show-parameters` as being in preview, appearing with the `[Preview]` tag.
***Preview Extensions***
Extensions are marked as being in preview using an older mechanism in the `azext_metadata.json` file.
```
{
"azext.isPreview": true,
}
```
It is recommended that, if an extension is in preview, that it also uses the above mechanisms to give the same level of visibility to in preview items.
|