Customization

What are our solutions to achieve customized logic?

Normally, there are two ways to do customization:

  • Inheritance: Inherit aaz commands and define customized logic in various callback actions. E.g.,
      class AddressPoolCreate(_AddressPoolCreate):
          @classmethod
          def _build_arguments_schema(cls, *args, **kwargs):
              from azure.cli.core.aaz import AAZListArg, AAZStrArg
    
              args_schema = super()._build_arguments_schema(*args, **kwargs)
              args_schema.servers = AAZListArg(
                  options=["--servers"],
                  help="Space-separated list of IP addresses or DNS names corresponding to backend servers."
              )
              args_schema.servers.Element = AAZStrArg()
              args_schema.backend_addresses._registered = False
    
              return args_schema
    
          def pre_operations(self):
              args = self.ctx.args
    
              def server_trans(_, server):
                  try:
                      socket.inet_aton(str(server))
                      return {"ip_address": server}
                  except socket.error:
                      return {"fqdn": server}
    
              args.backend_addresses = assign_aaz_list_arg(
                  args.backend_addresses,
                  args.servers,
                  element_transformer=server_trans
              )
    

    For more details, please visit GitHub.

    After that, please don’t forget to add your customized command to our command table in commands.py:

      def load_command_table(self, _):
          with self.command_group("network application-gateway address-pool"):
              from .custom import AddressPoolCreate, AddressPoolUpdate
              self.command_table["network application-gateway address-pool create"] = AddressPoolCreate(loader=self)
    

    For more details, please visit GitHub.

  • Wrapper: Call aaz commands within previous implementation. E.g.,
      def remove_ag_identity(cmd, resource_group_name, application_gateway_name, no_wait=False):
          class IdentityRemove(_ApplicationGatewayUpdate):
              def pre_operations(self):
                  args = self.ctx.args
                  args.no_wait = no_wait
    
              def pre_instance_update(self, instance):
                  instance.identity = None
    
          return IdentityRemove(cli_ctx=cmd.cli_ctx)(command_args={
              "name": application_gateway_name,
              "resource_group": resource_group_name
          })
    

    For more details, please visit GitHub.

    After that, please don’t forget to add your customized command to our command table in commands.py:

      def load_command_table(self, _):
          with self.command_group("network application-gateway identity") as g:
              g.custom_command("remove", "remove_ag_identity", supports_no_wait=True)
    

    For more details, please visit GitHub.

We always prefer to the inheritance way, which is more elegant and easier to maintain. Unless you wanna reuse previous huge complicated logic or features that aaz-dev hasn’t touched, we can consider the wrapper way (probably happens when migrating to aaz-dev).

For better understanding, you can firstly go through the above two examples. BTW, if there is no customization in your previous commands, aaz-dev is already naturally supported.

How many callback actions are there in the codegen framework?

There are eight callback actions in total:

  • For normal commands we have pre_operations and post_operations:
    • pre_operations: Usually used to implement some validation logic, which will be described in detail below.
    • post_operations: Not commonly used, but can be used to output logs when the operation is completed.
  • For update commands we have two more callback actions: pre_instance_update and post_instance_update:
    • pre_instance_update: Usually used to add some complicated customized logic to the instance.
    • post_instance_update: Usually used to clean up the redundant properties, which will be described in detail below.
  • For subcommands, there are four additional ones: pre_instance_create, post_instance_create, pre_instance_delete and post_instance_delete. Their functionalities are similar to the instance-related callback actions within the update command.

How to add validation to your commands?

You can leverage our inheritance solution, i.e., inherit the generated class in custom.py file and overwrite its pre_operations method:

from .aaz.latest.network.application_gateway.url_path_map.rule import Create as _URLPathMapRuleCreate

class URLPathMapRuleCreate(_URLPathMapRuleCreate):
    def pre_operations(self):
        args = self.ctx.args
        if has_value(args.address_pool) and has_value(args.redirect_config):
            err_msg = "Cannot reference a BackendAddressPool when Redirect Configuration is specified."
            raise ArgumentUsageError(err_msg)

For more details, please visit GitHub.

How to clean up redundant properties?

Usually, we can remove useless properties in post_instance_update:

def post_instance_update(self, instance):
    if not has_value(instance.properties.network_security_group.id):
        instance.properties.network_security_group = None
    if not has_value(instance.properties.route_table.id):
        instance.properties.route_table = None

For more details, please visit GitHub.

How to trim the output of a command?

As our code generation is written in Python, the output can be easily modified:

from .aaz.latest.network.lb import Update

def foo(cmd):
    result = Update(cli_ctx=cmd.cli_ctx)(command_args={
        "name": load_balancer_name,
        "resource_group": resource_group_name,
        "probes": probes,
    }).result()["probes"]
    
    return [r for r in result if r["name"] == item_name][0]

Can we add an extra output for a command?

If it is a simple operation, we can achieve that by rewriting _output method:

def _output(self, *args, **kwargs):
    result = self.deserialize_output(self.ctx.selectors.subresource.required(), client_flatten=True)
    
    return result

If it is a long-running operation, we can do that:

def _handler(self, command_args):
    lro_poller = super()._handler(command_args)
    lro_poller._result_callback = self._output

    return lro_poller

def _output(self, *args, **kwargs):
    result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True)
    
    return result

For more details, please visit GitHub.

How to support cross-subscription or cross-tenant?

It can be easily implemented by codegen framework, just declare the format of a parameter via AAZResourceIdArgFormat which will handle the cross-subscription/tenant ID from the argument. The template will auto complete the ID value from the placeholder names:

@classmethod
def _build_arguments_schema(cls, *args, **kwargs):
    from azure.cli.core.aaz import AAZResourceIdArgFormat
    
    args_schema = super()._build_arguments_schema(*args, **kwargs)
    args_schema.frontend_ip._fmt = AAZResourceIdArgFormat(
        template="/subscriptions/{subscription}/resourceGroups/{resource_group}/providers/Microsoft.Network/applicationGateways/{gateway_name}/frontendIPConfigurations/{}"
    )

    return args_schema

How to hide a command or a parameter to the users (only used for implementation)?

To hide a command, you can unregister a command in the CLI page; To hide a parameter:

@classmethod
def _build_arguments_schema(cls, *args, **kwargs):
    args_schema.protocol._registered = False

    return args_schema

If the parameter is required, we need to firstly declare _required to be False:

@classmethod
def _build_arguments_schema(cls, *args, **kwargs):
    args_schema.protocol._required = False
    args_schema.protocol._registered = False

    return args_schema

How to achieve a long-running operation based on codegen?

This kind of logic is often added to the custom function in custom.py:

def foo(cli_ctx):
    from azure.cli.core.commands import LongRunningOperation
    
    poller = VNetSubnetCreate(cli_ctx=cli_ctx)(command_args={
        "name": subnet_name,
        "vnet_name": metadata["name"],
        "resource_group": metadata["resource_group"],
        "address_prefix": args.subnet_prefix,
        "private_link_service_network_policies": "Disabled"
    })
    
    LongRunningOperation(cli_ctx)(poller)

For more details, please visit GitHub.

How to declare a file type argument?

It is nothing special, similar as other types of parameters:

class FOO(_FOO):
    @classmethod
    def _build_arguments_schema(cls, *args, **kwargs):
        from azure.cli.core.aaz import AAZFileArg, AAZFileArgBase64EncodeFormat
    
        args_schema = super()._build_arguments_schema(*args, **kwargs)
        args_schema.cert_file = AAZFileArg(
            options=["--cert-file"],
            help="Path to the pfx certificate file.",
            fmt=AAZFileArgBase64EncodeFormat(),
            nullable=True,  # commonly used in update command
        )
        args_schema.data._registered = False
    
        return args_schema
    
    def pre_operations(self):
        args = self.ctx.args
        if has_value(args.cert_file):
            args.data = args.cert_file

For more details, please visit GitHub.

How to show a secret property in the output?

The hide of secret properties in output is by design, but we still support to display them through rewriting _output method:

def _output(self, *args, **kwargs):
    result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True, secret_hidden=False)

    return result

How to elegantly remove the generated codes?

We can achieve that on the CLI page, please check the details. Some other documents are also helpful to understand the interation logic of the codegen UI.

Is null automatically ignored in the output of the codegen commands?

Yes, it’s by design, and we need to be consistent with the definition in the OpenAPI specification.