Skip to content

Latest commit

 

History

History
959 lines (692 loc) · 50 KB

authoring_commands.md

File metadata and controls

959 lines (692 loc) · 50 KB

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

1. Write the Command Loader

2. Write a Command

3. Register Commands

4. Write Help Entry

5. Customize Arguments

ADDITIONAL TOPICS

6. Keyword Argument (kwarg) Reference

7. Supporting the IDs Parameter

8. Supporting Name or ID Parameters

9. Generic Update Commands

10. Custom Table Formats

11. Tab Completion (bash only)

12. Validators

13. Registering Flag Arguments

14. Registering Enum Arguments

15. Preventing particular extensions from being loading

16. Deprecating Commands and Arguments

17. Multi-API Aware Modules

18. 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:

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 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:

# (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: 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

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

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 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:

   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 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()

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.

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:

    # (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 commands az mypackage command1 and az mypackage command2, which both have a parameter my_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 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

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.

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
  • validator - See section on 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
  • 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

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
  • completer - See section on Tab Completion
  • id_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 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

Additionally, the following kwargs, supported by argparse, are supported as well:

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.

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 on list 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', ...)

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

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
            )

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:

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 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:

    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:

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:
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:

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

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

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).

Registering Flags

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.

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.

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.

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

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.

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:
class ResourceType(Enum):

  MGMT_MYSERVICE = ('azure.mgmt.myservice, MyServiceManagementClient')  # REGISTER YOUR SDK
  ...
  1. 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
    ...
  },
  ...
}
  1. 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
  1. 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.

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

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.