From 845503fca651ba57fd97e4e2d0ac37a37b6bd54f Mon Sep 17 00:00:00 2001 From: necusjz Date: Fri, 21 Jun 2024 14:51:20 +0800 Subject: [PATCH 1/8] docs: add customization document --- docs/pages/usage/customization.md | 226 ++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/pages/usage/customization.md diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md new file mode 100644 index 00000000..f433ca37 --- /dev/null +++ b/docs/pages/usage/customization.md @@ -0,0 +1,226 @@ +--- +layout: page +title: Customization +permalink: /pages/usage/customization/ +weight: 104 +--- + +## 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., + ```python + 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 + ) + ``` + After that, please don't forget to add your customized command to our command table in _commands.py_: + ```python + 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) + ``` +- Wrapper: Call aaz commands within previous implementation. E.g., + ```python + 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 + }) + ``` + After that, please don't forget to add your customized command to our command table in _commands.py_: + ```python + def load_command_table(self, _): + with self.command_group("network application-gateway identity") as g: + from .custom import IdentityAssign + g.custom_command("remove", "remove_ag_identity", supports_no_wait=True) + ``` + +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`. +- For update commands we have two more callback actions: `pre_instance_update` and `post_instance_update`. +- For subcommands, there are four additional ones: `pre_instance_create`, `post_instance_create`, `pre_instance_delete` and `post_instance_delete`. + +## 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: +```python +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) +``` + +## How to clean up redundant properties? +Usually, we can remove useless properties in `post_instance_update`: +```python +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 +``` + +## How to trim the output of a command? +As our code generation is written in Python, the output can be easily modified: +```python +from .aaz.latest.network.lb import Update + +def foo(): + 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: +```python +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: +```python +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 +``` + +## How to support cross-subscription or cross-tenant? +It can be easily implemented by codegen framework, just declare the format of a parameter via template: +```python +@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: +```python +@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: +```python +@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? +It is similar as previous manual solution: +```python +from azure.cli.core.commands import LongRunningOperation + +poller = VNetSubnetCreate(cli_ctx=self.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(self.cli_ctx)(poller) +``` + +## How to declare a file type argument? +It is nothing special, similar as others: +```python +@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 +``` + +## How to show a secret property in the output? +The hidden of secret properties in output is by design, but we still support to show that: +```python +def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True, secret_hidden=False) + + return result +``` From 193ad11e3facbbfb900b776ed31622872cd9e1d8 Mon Sep 17 00:00:00 2001 From: necusjz Date: Mon, 24 Jun 2024 14:18:36 +0800 Subject: [PATCH 2/8] Update docs/pages/usage/customization.md Co-authored-by: kai ru <69238381+kairu-ms@users.noreply.github.com> --- docs/pages/usage/customization.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index f433ca37..4fe54604 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -67,7 +67,6 @@ Normally, there are two ways to do customization: ```python def load_command_table(self, _): with self.command_group("network application-gateway identity") as g: - from .custom import IdentityAssign g.custom_command("remove", "remove_ag_identity", supports_no_wait=True) ``` From 6d3081f245da3645507578fd6b1133028e1f0f7d Mon Sep 17 00:00:00 2001 From: necusjz Date: Mon, 24 Jun 2024 16:06:34 +0800 Subject: [PATCH 3/8] chore: resolve comments --- docs/pages/usage/customization.md | 82 +++++++++++++++++-------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index 4fe54604..bc9930a3 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -76,9 +76,13 @@ For better understanding, you can firstly go through the above two examples. BTW ## 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`. -- For update commands we have two more callback actions: `pre_instance_update` and `post_instance_update`. -- For subcommands, there are four additional ones: `pre_instance_create`, `post_instance_create`, `pre_instance_delete` and `post_instance_delete`. +- 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: @@ -141,7 +145,7 @@ def _output(self, *args, **kwargs): ``` ## How to support cross-subscription or cross-tenant? -It can be easily implemented by codegen framework, just declare the format of a parameter via template: +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: ```python @classmethod def _build_arguments_schema(cls, *args, **kwargs): @@ -156,7 +160,7 @@ def _build_arguments_schema(cls, *args, **kwargs): ``` ## 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: +To hide a command, you can [unregister a command](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#unregistered-commands) in the CLI page; To hide a parameter: ```python @classmethod def _build_arguments_schema(cls, *args, **kwargs): @@ -176,47 +180,49 @@ def _build_arguments_schema(cls, *args, **kwargs): ``` ## How to achieve a long-running operation based on codegen? -It is similar as previous manual solution: +This kind of logic is often added to the custom function in _custom.py_: ```python -from azure.cli.core.commands import LongRunningOperation - -poller = VNetSubnetCreate(cli_ctx=self.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(self.cli_ctx)(poller) +def foo(): + from azure.cli.core.commands import LongRunningOperation + + poller = VNetSubnetCreate(cli_ctx=self.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(self.cli_ctx)(poller) ``` ## How to declare a file type argument? -It is nothing special, similar as others: +It is nothing special, similar as other types of parameters: ```python -@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 +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 ``` ## How to show a secret property in the output? -The hidden of secret properties in output is by design, but we still support to show that: +The hide of secret properties in output is by design, but we still support to show them through rewriting `_output` method: ```python def _output(self, *args, **kwargs): result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True, secret_hidden=False) From a61d4a8cdf81a69ac8e0bf8cf436bbb4595e98f3 Mon Sep 17 00:00:00 2001 From: necusjz Date: Mon, 24 Jun 2024 17:01:44 +0800 Subject: [PATCH 4/8] docs: add two more faqs --- docs/pages/usage/customization.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index bc9930a3..f5779d62 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -222,10 +222,16 @@ class FOO(_FOO): ``` ## How to show a secret property in the output? -The hide of secret properties in output is by design, but we still support to show them through rewriting `_output` method: +The hide of secret properties in output is by design, but we still support to display them through rewriting `_output` method: ```python 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, check [the details](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#remove-commands). Some [other documents](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#pick-commands) are also helpful to understand the interation logic of 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. From 0b87c11dfc91521c9e8f1986fab0cb0ceb9d7490 Mon Sep 17 00:00:00 2001 From: necusjz Date: Mon, 24 Jun 2024 17:06:02 +0800 Subject: [PATCH 5/8] chore: correct grammar --- docs/pages/usage/customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index f5779d62..3baec4ec 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -231,7 +231,7 @@ def _output(self, *args, **kwargs): ``` ## How to elegantly remove the generated codes? -We can achieve that on the CLI page, check [the details](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#remove-commands). Some [other documents](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#pick-commands) are also helpful to understand the interation logic of codegen UI. +We can achieve that on the CLI page, please check [the details](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#remove-commands). Some [other documents](https://azure.github.io/aaz-dev-tools/pages/usage/cli-generator/#pick-commands) 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. From 9a580db073497ff44d6979d127a526c5af458088 Mon Sep 17 00:00:00 2001 From: necusjz Date: Tue, 25 Jun 2024 11:03:04 +0800 Subject: [PATCH 6/8] Update docs/pages/usage/customization.md Co-authored-by: kai ru <69238381+kairu-ms@users.noreply.github.com> --- docs/pages/usage/customization.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index 3baec4ec..c82e0944 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -182,10 +182,10 @@ def _build_arguments_schema(cls, *args, **kwargs): ## How to achieve a long-running operation based on codegen? This kind of logic is often added to the custom function in _custom.py_: ```python -def foo(): +def foo(cli_ctx): from azure.cli.core.commands import LongRunningOperation - poller = VNetSubnetCreate(cli_ctx=self.cli_ctx)(command_args={ + poller = VNetSubnetCreate(cli_ctx=cli_ctx)(command_args={ "name": subnet_name, "vnet_name": metadata["name"], "resource_group": metadata["resource_group"], @@ -193,7 +193,7 @@ def foo(): "private_link_service_network_policies": "Disabled" }) - LongRunningOperation(self.cli_ctx)(poller) + LongRunningOperation(cli_ctx)(poller) ``` ## How to declare a file type argument? From 85624c6fc688ca912f40f248493d69c8faba607c Mon Sep 17 00:00:00 2001 From: necusjz Date: Tue, 25 Jun 2024 11:30:00 +0800 Subject: [PATCH 7/8] Update docs/pages/usage/customization.md Co-authored-by: kai ru <69238381+kairu-ms@users.noreply.github.com> --- docs/pages/usage/customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index c82e0944..b9a100f3 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -112,7 +112,7 @@ As our code generation is written in Python, the output can be easily modified: ```python from .aaz.latest.network.lb import Update -def foo(): +def foo(cmd): result = Update(cli_ctx=cmd.cli_ctx)(command_args={ "name": load_balancer_name, "resource_group": resource_group_name, From 442e2a86fc8fb4174cd2fb243cb1c150dafea153 Mon Sep 17 00:00:00 2001 From: necusjz Date: Wed, 26 Jun 2024 10:20:29 +0800 Subject: [PATCH 8/8] docs: add permalink for snippets --- docs/pages/usage/customization.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/pages/usage/customization.md b/docs/pages/usage/customization.md index b9a100f3..dbee6fe0 100644 --- a/docs/pages/usage/customization.md +++ b/docs/pages/usage/customization.md @@ -40,6 +40,8 @@ Normally, there are two ways to do customization: element_transformer=server_trans ) ``` + > For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L413-L440). + After that, please don't forget to add your customized command to our command table in _commands.py_: ```python def load_command_table(self, _): @@ -47,6 +49,7 @@ Normally, there are two ways to do customization: from .custom import AddressPoolCreate, AddressPoolUpdate self.command_table["network application-gateway address-pool create"] = AddressPoolCreate(loader=self) ``` + > For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/commands.py#L54-L57). - Wrapper: Call aaz commands within previous implementation. E.g., ```python def remove_ag_identity(cmd, resource_group_name, application_gateway_name, no_wait=False): @@ -63,12 +66,15 @@ Normally, there are two ways to do customization: "resource_group": resource_group_name }) ``` + > For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L737-L749). + After that, please don't forget to add your customized command to our command table in _commands.py_: ```python 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](https://github.com/Azure/azure-cli/blob/dev/src/azure-cli/azure/cli/command_modules/network/commands.py#L99-L102). 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). @@ -96,6 +102,7 @@ class URLPathMapRuleCreate(_URLPathMapRuleCreate): err_msg = "Cannot reference a BackendAddressPool when Redirect Configuration is specified." raise ArgumentUsageError(err_msg) ``` +> For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L1808-L1841). ## How to clean up redundant properties? Usually, we can remove useless properties in `post_instance_update`: @@ -106,6 +113,7 @@ def post_instance_update(self, instance): if not has_value(instance.properties.route_table.id): instance.properties.route_table = None ``` +> For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L5755-L5761). ## How to trim the output of a command? As our code generation is written in Python, the output can be easily modified: @@ -143,6 +151,7 @@ def _output(self, *args, **kwargs): return result ``` +> For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L6393-L6409). ## 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: @@ -195,6 +204,7 @@ def foo(cli_ctx): LongRunningOperation(cli_ctx)(poller) ``` +> For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L752-L858). ## How to declare a file type argument? It is nothing special, similar as other types of parameters: @@ -220,6 +230,7 @@ class FOO(_FOO): if has_value(args.cert_file): args.data = args.cert_file ``` +> For more details, please visit [GitHub](https://github.com/Azure/azure-cli/blob/1d5f9d5ebd3595f32212ec7ea4309267a288b9eb/src/azure-cli/azure/cli/command_modules/network/custom.py#L392-L410). ## 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: