The document provides instructions and guidelines on how to author individual commands.
Note
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.
AUTHORING COMMANDS
ADDITIONAL TOPICS
6. Keyword Argument (kwarg) Reference
7. Supporting the IDs Parameter
8. Supporting Name or ID Parameters
11. Tab Completion (bash only)
13. Registering Flag Arguments
14. Registering Enum Arguments
15. Preventing particular extensions from being loading
16. Deprecating Commands and Arguments
18. Preview Commands and Arguments
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:
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
class ResourceType(Enum): # pylint: disable=too-few-public-methods
...
MGMT_MYTYPE = ('azure.mgmt.mytype', 'MyTypeManagementClient')
...
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.
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:
# (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
command_group(self, group_name, command_type=None, **kwargs)
group_name
: the group name ('network', 'storage account', etc.)command_type
: aCliCommandType
object that will be used for all calls tocommand
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) andclient_factory
(if custom commands use theclient
argument).
(3) Command Registration Helpers
command
command(self, name, method_name=None, command_type=None, **kwargs)
name
: The name of the command within the command groupmethod_name
: The name of the SDK or custom method, relative to the path specified inoperations_tmpl
.command_type
: ACliCommandType
object to apply to this command (optional).kwargs
: any supported kwarg. Commonly used kwargs includevalidator
,table_transformer
,confirmation
,supports_no_wait
andtransform
.
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
wait_command
The generic wait command provides a template solution for polling Azure resources until specific conditions are met.
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 inoperations_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:
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.
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 inoperations_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()
with self.command_group('mymod', mymod_sdk) as g:
g.command('command1', 'do_something_1', supports_no_wait=True)
custom_command()
# 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:
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()
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
Here are examples:
custom_command()
# 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.
See the following for guidance on writing a help entry: https://github.com/Azure/azure-cli/blob/master/doc/authoring_help.md
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:
# (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
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 commandsaz mypackage command1
andaz mypackage command2
, which both have a parametermy_param
.
with self.argument_context('mypackage', ...) as c: # applies to BOTH command1 and command2
But
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.
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
argument(self, dest, arg_type=None, **kwargs)
dest
- The name of the parameter that you are targeting.arg_type
- An instance of theazure.cli.core.commands.CliArgumentType
class. This essentially serves as a named, reusable packaging of thekwargs
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. Anykwargs
specified will override or extended the definition inarg_type
, if provided.
ignore
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
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.
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 Formatsvalidator
- See section on Validatorsconfirmation
- 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 Argumentsformatter_class
- Advanced. Accepts a custom class that derives fromargparse.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
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
- Anazure.cli.core.profiles.ResourceType
enum value that is used for multi-API packages.operation_group
- Only used by theazure-cli-vm
module to specify which resource API to target.command_group
- ACliCommandType
object that contains a bundle of kwargs that will be used by thecommand
method if not otherwise provided.custom_command_group
- ACliCommandType
object that contains a bundle of kwargs that will be used by thecustom_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 theoptions_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']
oroptions_list='--myparam'
)validator
- See section on Validatorscompleter
- See section on Tab Completionid_part
- See section on 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 ifarg_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
Additionally, the following kwargs
, supported by argparse, are supported as well:
nargs
- See https://docs.python.org/3/library/argparse.html#nargsaction
- See https://docs.python.org/3/library/argparse.html#actionconst
- See https://docs.python.org/3/library/argparse.html#constdefault
- 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#typechoices
- 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
- Anazure.cli.core.profiles.ResourceType
enum value that is used for multi-API packages.operation_group
- Only used by theazure-cli-vm
module to specify which resource API to target.
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.
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
:
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:
{
"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 onlist
commands for child resources. This simplest way to do this:
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', ...)
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
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
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
)
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:
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:
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 namedget
(which is the case for most SDKs), this can be omitted. -
getter_type
- ACliCommandType
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 namedcreate_or_update
(which is the case for most SDKs), this can be omitted. -
setter_type
- ACliCommandType
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 isparameters
(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 namedinstance
), 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: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 bemy_custom_function
. -
custom_func_type
- ACliCommandType
object which will be used to locate the custom function. If omitted, the CLI will look for and attempt to use thecustom_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 accessmy_child
, the value would bemy_child
. To accessmy_grandchild
, the value would bemy_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 isname
. To refer tomy_child
, the value would bename
. To refer tomy_grandchild
the value would bename.name
.child_arg_name
- If you want to refer the child object key (the property identified bychild_collection_key
) inside a custom function, you should specify the argument name you use in your custom function. By default, this is calleditem_name
. In the above example, where our child object had a key calledname
, you could refer to this property within your custom function through theitem_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:
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 beupdate
instead ofcreate_or_update
(the default). - You will HAVE TO supply
custom_func_name
andcustom_func_type
. Consider the following example:
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
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:
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:
table_transformer='{Name:name, ResourceGroup:resourceGroup, Location:location, ProvisioningState:provisioningState, PowerState:instanceView.statuses[1].displayStatus}'
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:
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
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 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:
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:
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).
The recommended way to register boolean properties in the CLI is using the get_three_state_flag
arg_type.
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:
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.
The recommended way to register enums (either reflected from an SDK or custom choice lists) is to use the get_enum_type
arg_type.
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.
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.
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))
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 booleanTrue
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
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
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
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
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.
To convert a module that used a mono-versioned SDK to one that works with multiple API versions:
- In
azure.cli.core.profiles._shared.py
register your SDK and client in theResourceType
enum:
class ResourceType(Enum):
MGMT_MYSERVICE = ('azure.mgmt.myservice, MyServiceManagementClient') # REGISTER YOUR SDK
...
- In the
AZURE_API_PROFILES
dictionary in that same file, for each profile your service applies to, add an entry for it like this:
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
...
},
...
}
- 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:
from azure.mgmt.myservice import Foo, Boo
def my_command(...):
# do stuff
Converted:
def my_command(cmd, ...):
Foo, Boo = cmd.get_models('Foo', 'Boo')
# do stuff
- Use appropriate conditionals to ensure your command can run on all supported profiles:
commands.py
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
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
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.
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
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
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
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.